├── .devcontainer ├── .dockerignore ├── Dockerfile ├── README.md └── devcontainer.json ├── .dockerignore ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ ├── feature_request.yml │ └── provider.md ├── dependabot.yml ├── labels.yml └── workflows │ ├── ci-skip.yml │ ├── ci.yml │ ├── closed-issue.yml │ ├── configs │ └── mlc-config.json │ ├── labels.yml │ ├── markdown-skip.yml │ ├── markdown.yml │ └── opened-issue.yml ├── .gitignore ├── .golangci.yml ├── .markdownlint.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── cmd └── gluetun │ └── main.go ├── doc ├── logo.svg └── logo_256.png ├── go.mod ├── go.sum ├── internal ├── alpine │ ├── alpine.go │ ├── users.go │ └── version.go ├── cli │ ├── ci.go │ ├── cli.go │ ├── clientkey.go │ ├── formatservers.go │ ├── genkey.go │ ├── healthcheck.go │ ├── interfaces.go │ ├── nooplogger.go │ ├── openvpnconfig.go │ ├── update.go │ └── warner.go ├── command │ ├── cmder.go │ ├── interfaces_local.go │ ├── mocks_generate_test.go │ ├── mocks_local_test.go │ ├── run.go │ ├── run_test.go │ ├── split.go │ ├── split_test.go │ ├── start.go │ └── start_test.go ├── configuration │ ├── settings │ │ ├── deprecated.go │ │ ├── dns.go │ │ ├── dnsblacklist.go │ │ ├── dot.go │ │ ├── errors.go │ │ ├── firewall.go │ │ ├── firewall_test.go │ │ ├── health.go │ │ ├── healthywait.go │ │ ├── helpers.go │ │ ├── helpers │ │ │ └── belong.go │ │ ├── helpers_test.go │ │ ├── httpproxy.go │ │ ├── interfaces.go │ │ ├── log.go │ │ ├── mocks_generate_test.go │ │ ├── mocks_reader_test.go │ │ ├── mocks_test.go │ │ ├── nordvpn_retro.go │ │ ├── openvpn.go │ │ ├── openvpn_test.go │ │ ├── openvpnselection.go │ │ ├── portforward.go │ │ ├── portforward_test.go │ │ ├── provider.go │ │ ├── publicip.go │ │ ├── publicip_test.go │ │ ├── server.go │ │ ├── serverselection.go │ │ ├── settings.go │ │ ├── settings_test.go │ │ ├── shadowsocks.go │ │ ├── storage.go │ │ ├── surfshark_retro.go │ │ ├── system.go │ │ ├── updater.go │ │ ├── validation │ │ │ ├── servers.go │ │ │ └── surfshark.go │ │ ├── version.go │ │ ├── vpn.go │ │ ├── wireguard.go │ │ └── wireguardselection.go │ └── sources │ │ ├── files │ │ ├── helpers.go │ │ ├── interfaces.go │ │ ├── reader.go │ │ ├── wireguard.go │ │ └── wireguard_test.go │ │ └── secrets │ │ ├── helpers.go │ │ ├── interfaces.go │ │ ├── reader.go │ │ ├── reader_test.go │ │ └── wireguard.go ├── constants │ ├── colors.go │ ├── countries.go │ ├── openvpn │ │ ├── auth.go │ │ ├── ciphers.go │ │ ├── paths.go │ │ └── versions.go │ ├── paths.go │ ├── protocol.go │ ├── providers │ │ ├── providers.go │ │ └── providers_test.go │ ├── status.go │ └── vpn │ │ └── protocol.go ├── dns │ ├── logger.go │ ├── loop.go │ ├── plaintext.go │ ├── run.go │ ├── settings.go │ ├── setup.go │ ├── state │ │ ├── settings.go │ │ └── state.go │ ├── status.go │ ├── ticker.go │ └── update.go ├── firewall │ ├── cmd_matcher_test.go │ ├── delete.go │ ├── delete_test.go │ ├── enable.go │ ├── firewall.go │ ├── interfaces.go │ ├── ip6tables.go │ ├── iptables.go │ ├── iptablesmix.go │ ├── list.go │ ├── list_test.go │ ├── logger.go │ ├── mocks_generate_test.go │ ├── mocks_test.go │ ├── outboundsubnets.go │ ├── parse.go │ ├── parse_test.go │ ├── ports.go │ ├── redirect.go │ ├── support.go │ ├── support_test.go │ └── vpn.go ├── format │ ├── duration.go │ └── duration_test.go ├── healthcheck │ ├── client.go │ ├── handler.go │ ├── health.go │ ├── health_test.go │ ├── logger.go │ ├── openvpn.go │ ├── run.go │ └── server.go ├── httpproxy │ ├── accept.go │ ├── auth.go │ ├── handler.go │ ├── handler_test.go │ ├── http.go │ ├── https.go │ ├── logger.go │ ├── loop.go │ ├── run.go │ ├── server.go │ ├── settings.go │ ├── state │ │ ├── settings.go │ │ └── state.go │ └── status.go ├── httpserver │ ├── address.go │ ├── helpers_test.go │ ├── logger.go │ ├── logger_mock_test.go │ ├── run.go │ ├── run_test.go │ ├── server.go │ ├── server_test.go │ ├── settings.go │ └── settings_test.go ├── loopstate │ ├── apply.go │ ├── get.go │ ├── lock.go │ ├── set.go │ └── state.go ├── mod │ ├── info.go │ ├── load.go │ └── probe.go ├── models │ ├── alias.go │ ├── build.go │ ├── connection.go │ ├── filters.go │ ├── markdown.go │ ├── markdown_test.go │ ├── publicip.go │ ├── server.go │ ├── server_test.go │ ├── servers.go │ ├── servers_test.go │ └── sort.go ├── natpmp │ ├── checks.go │ ├── checks_test.go │ ├── externaladdress.go │ ├── externaladdress_test.go │ ├── helpers_test.go │ ├── natpmp.go │ ├── natpmp_test.go │ ├── portmapping.go │ ├── portmapping_test.go │ ├── rpc.go │ └── rpc_test.go ├── netlink │ ├── address.go │ ├── address_unspecified.go │ ├── conversion.go │ ├── conversion_test.go │ ├── family.go │ ├── helpers_test.go │ ├── interfaces.go │ ├── ipv6.go │ ├── link.go │ ├── link_unspecified.go │ ├── netlink.go │ ├── route.go │ ├── route_unspecified.go │ ├── rule.go │ ├── rule_test.go │ ├── rule_unspecified.go │ ├── types.go │ ├── wireguard.go │ ├── wireguard_test.go │ └── wireguard_unspecified.go ├── openvpn │ ├── auth.go │ ├── config.go │ ├── extract │ │ ├── data.go │ │ ├── extract.go │ │ ├── extract_test.go │ │ ├── extractor.go │ │ ├── helpers_test.go │ │ ├── pem.go │ │ ├── pem_test.go │ │ ├── read.go │ │ └── read_test.go │ ├── interfaces.go │ ├── logger.go │ ├── logs.go │ ├── logs_test.go │ ├── openvpn.go │ ├── paths.go │ ├── pkcs8 │ │ ├── algorithms.go │ │ ├── algorithms_test.go │ │ ├── descbc.go │ │ ├── testdata │ │ │ ├── readme.txt │ │ │ ├── rsa_pkcs8_aes128cbc_decrypted.pem │ │ │ ├── rsa_pkcs8_aes128cbc_encrypted.pem │ │ │ ├── rsa_pkcs8_descbc_decrypted.pem │ │ │ └── rsa_pkcs8_descbc_encrypted.pem │ │ ├── upgrade.go │ │ └── upgrade_test.go │ ├── run.go │ ├── start.go │ ├── stream.go │ └── version.go ├── portforward │ ├── interfaces.go │ ├── loop.go │ ├── service │ │ ├── command.go │ │ ├── command_test.go │ │ ├── fs.go │ │ ├── helpers.go │ │ ├── helpers_test.go │ │ ├── interfaces.go │ │ ├── mocks_generate_test.go │ │ ├── mocks_test.go │ │ ├── service.go │ │ ├── settings.go │ │ ├── start.go │ │ └── stop.go │ └── settings.go ├── pprof │ ├── helpers_test.go │ ├── logger_mock_test.go │ ├── server.go │ ├── server_test.go │ ├── settings.go │ └── settings_test.go ├── provider │ ├── airvpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── common │ │ ├── mocks.go │ │ ├── mocks_generate_test.go │ │ ├── portforward.go │ │ ├── storage.go │ │ └── updater.go │ ├── custom │ │ ├── connection.go │ │ ├── interfaces.go │ │ ├── openvpnconf.go │ │ ├── openvpnconf_test.go │ │ └── provider.go │ ├── cyberghost │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── constants.go │ │ │ ├── countries.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── example │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── expressvpn │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── hardcoded.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── fastestvpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── api_test.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── giganews │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── filename.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── hidemyass │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── hosts.go │ │ │ ├── hosttourl.go │ │ │ ├── index.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ ├── updater.go │ │ │ └── url.go │ ├── ipvanish │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── filename.go │ │ │ ├── filename_test.go │ │ │ ├── hosttoserver.go │ │ │ ├── hosttoserver_test.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ ├── servers_test.go │ │ │ └── updater.go │ ├── ivpn │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── api_test.go │ │ │ ├── resolve.go │ │ │ ├── roundtrip_test.go │ │ │ ├── servers.go │ │ │ ├── servers_test.go │ │ │ └── updater.go │ ├── mullvad │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── hosttoserver.go │ │ │ ├── ips.go │ │ │ ├── ips_test.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── nordvpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── models.go │ │ │ ├── name.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── perfectprivacy │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── portforward.go │ │ ├── portforward_test.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── citytoserver.go │ │ │ ├── filename.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── privado │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── hosttoserver.go │ │ │ ├── location.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── privateinternetaccess │ │ ├── connection.go │ │ ├── httpclient.go │ │ ├── httpclient_test.go │ │ ├── openvpnconf.go │ │ ├── portforward.go │ │ ├── portforward_test.go │ │ ├── presets │ │ │ └── presets.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── hosttoserver.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── privatevpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── portforward.go │ │ ├── portforward_test.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── countries.go │ │ │ ├── filename.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── protonvpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── portforward.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── countries.go │ │ │ ├── iptoserver.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── provider.go │ ├── providers.go │ ├── purevpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── slickvpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── helpers_test.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ ├── testdata │ │ │ └── index.html │ │ │ ├── updater.go │ │ │ ├── website.go │ │ │ └── website_test.go │ ├── surfshark │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ ├── servers │ │ │ └── locationdata.go │ │ └── updater │ │ │ ├── api.go │ │ │ ├── api_test.go │ │ │ ├── hosttoserver.go │ │ │ ├── location.go │ │ │ ├── remaining.go │ │ │ ├── resolve.go │ │ │ ├── roundtrip_test.go │ │ │ ├── servers.go │ │ │ ├── updater.go │ │ │ └── zip.go │ ├── torguard │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── filename.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── utils │ │ ├── cipher.go │ │ ├── cipher_test.go │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── filtering.go │ │ ├── filtering_test.go │ │ ├── logger.go │ │ ├── nofetcher.go │ │ ├── openvpn.go │ │ ├── pick.go │ │ ├── pick_test.go │ │ ├── port.go │ │ ├── port_test.go │ │ ├── portforward.go │ │ ├── protocol.go │ │ ├── protocol_test.go │ │ ├── wireguard.go │ │ └── wireguard_test.go │ ├── vpnsecure │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── helpers_test.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ ├── testdata │ │ │ └── index.html │ │ │ ├── updater.go │ │ │ ├── website.go │ │ │ └── website_test.go │ ├── vpnunlimited │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── constants.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── vyprvpn │ │ ├── connection.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── filename.go │ │ │ ├── hosttoserver.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ ├── wevpn │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ │ ├── cities.go │ │ │ ├── hostname.go │ │ │ ├── resolve.go │ │ │ ├── servers.go │ │ │ └── updater.go │ └── windscribe │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── openvpnconf.go │ │ ├── provider.go │ │ └── updater │ │ ├── api.go │ │ ├── servers.go │ │ └── updater.go ├── publicip │ ├── api │ │ ├── api.go │ │ ├── api_test.go │ │ ├── cloudflare.go │ │ ├── echoip.go │ │ ├── errors.go │ │ ├── interfaces.go │ │ ├── ip2location.go │ │ ├── ipinfo.go │ │ ├── multi.go │ │ └── resilient.go │ ├── data.go │ ├── fs.go │ ├── interfaces.go │ ├── loop.go │ └── update.go ├── routing │ ├── conversion.go │ ├── conversion_test.go │ ├── default.go │ ├── enable.go │ ├── errors.go │ ├── inbound.go │ ├── ip.go │ ├── ip_test.go │ ├── local.go │ ├── logger.go │ ├── mocks_generate_test.go │ ├── mocks_test.go │ ├── outbound.go │ ├── routes.go │ ├── routing.go │ ├── rules.go │ ├── rules_test.go │ └── vpn.go ├── server │ ├── dns.go │ ├── handler.go │ ├── handlerv0.go │ ├── handlerv1.go │ ├── helpers.go │ ├── interfaces.go │ ├── logger.go │ ├── middlewares │ │ ├── auth │ │ │ ├── apikey.go │ │ │ ├── basic.go │ │ │ ├── configfile.go │ │ │ ├── configfile_test.go │ │ │ ├── format.go │ │ │ ├── interfaces.go │ │ │ ├── interfaces_local.go │ │ │ ├── lookup.go │ │ │ ├── lookup_test.go │ │ │ ├── middleware.go │ │ │ ├── middleware_test.go │ │ │ ├── mocks_generate_test.go │ │ │ ├── mocks_test.go │ │ │ ├── none.go │ │ │ └── settings.go │ │ └── log │ │ │ ├── interfaces.go │ │ │ └── middleware.go │ ├── openvpn.go │ ├── publicip.go │ ├── server.go │ ├── updater.go │ ├── vpn.go │ └── wrappers.go ├── shadowsocks │ ├── logger.go │ ├── loop.go │ └── state.go ├── storage │ ├── choices.go │ ├── copy.go │ ├── copy_test.go │ ├── filter.go │ ├── flush.go │ ├── formatting.go │ ├── hardcoded.go │ ├── hardcoded_test.go │ ├── helpers.go │ ├── merge.go │ ├── mocks_generate_test.go │ ├── mocks_test.go │ ├── read.go │ ├── read_test.go │ ├── servers.go │ ├── servers.json │ ├── storage.go │ └── sync.go ├── subnet │ └── subsets.go ├── tun │ ├── check.go │ ├── check_unspecified.go │ ├── create.go │ ├── create_unspecified.go │ ├── tun.go │ └── tun_test.go ├── updater │ ├── html │ │ ├── attribute.go │ │ ├── bfs.go │ │ ├── css.go │ │ ├── errors.go │ │ ├── fetch.go │ │ ├── fetch_test.go │ │ └── match.go │ ├── interfaces.go │ ├── loop │ │ ├── loop.go │ │ └── state.go │ ├── openvpn │ │ ├── extract.go │ │ ├── fetch.go │ │ └── multifetch.go │ ├── providers.go │ ├── resolver │ │ ├── ips.go │ │ ├── ips_test.go │ │ ├── net.go │ │ ├── parallel.go │ │ └── repeat.go │ ├── unzip │ │ ├── extract.go │ │ ├── fetch.go │ │ └── unzip.go │ └── updater.go ├── version │ ├── github.go │ └── version.go ├── vpn │ ├── cleanup.go │ ├── helpers.go │ ├── interfaces.go │ ├── loop.go │ ├── openvpn.go │ ├── portforward.go │ ├── run.go │ ├── settings.go │ ├── state │ │ ├── state.go │ │ └── vpn.go │ ├── status.go │ ├── tunnelup.go │ └── wireguard.go └── wireguard │ ├── address.go │ ├── address_test.go │ ├── cleanup.go │ ├── cleanup_test.go │ ├── config.go │ ├── config_test.go │ ├── constructor.go │ ├── constructor_test.go │ ├── helpers_test.go │ ├── log.go │ ├── log_mock_test.go │ ├── log_test.go │ ├── netlink_integration_test.go │ ├── netlinker.go │ ├── netlinker_mock_test.go │ ├── route.go │ ├── route_test.go │ ├── rule.go │ ├── rule_test.go │ ├── run.go │ ├── settings.go │ └── settings_test.go ├── maintenance.md └── title.svg /.devcontainer/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | devcontainer.json 3 | Dockerfile 4 | README.md 5 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM qmcgaw/godevcontainer:v0.20-alpine 2 | RUN apk add wireguard-tools htop openssl 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .devcontainer 2 | .git 3 | .github 4 | doc 5 | docker-compose.yml 6 | Dockerfile 7 | LICENSE 8 | README.md 9 | title.svg 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @qdm12 -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [open source license of this project](../LICENSE). 4 | 5 | ## Submitting a pull request 6 | 7 | 1. [Fork](https://github.com/qdm12/gluetun/fork) and clone the repository 8 | 1. Create a new branch `git checkout -b my-branch-name` 9 | 1. Modify the code 10 | 1. Ensure the docker build succeeds `docker build .` (you might need `export DOCKER_BUILDKIT=1`) 11 | 1. Commit your modifications 12 | 1. Push to your fork and [submit a pull request](https://github.com/qdm12/gluetun/compare) 13 | 14 | ## Resources 15 | 16 | - [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) 17 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 18 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [qdm12] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a Wiki issue 4 | url: https://github.com/qdm12/gluetun-wiki/issues/new/choose 5 | about: Please create an issue on the gluetun-wiki repository. 6 | - name: Configuration help? 7 | url: https://github.com/qdm12/gluetun/discussions/new/choose 8 | about: Please create a Github discussion. 9 | - name: Unraid template issue 10 | url: https://github.com/qdm12/gluetun/discussions/550 11 | about: Please read the relevant Github discussion. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature to add to Gluetun 3 | title: "Feature request: " 4 | labels: [":bulb: feature request"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: "What's the feature 🧐" 10 | placeholder: "Make the tunnel resistant to earth quakes" 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: extra 15 | attributes: 16 | label: "Extra information and references" 17 | placeholder: | 18 | - I tried `docker run something` and it doesn't work 19 | - That [url](https://github.com/qdm12/gluetun) is interesting 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/provider.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support a VPN provider 3 | about: Suggest a VPN provider to be supported 4 | title: 'VPN provider support: NAME OF THE PROVIDER' 5 | labels: ":bulb: New provider" 6 | 7 | --- 8 | 9 | One of the following is required: 10 | 11 | - Publicly accessible URL to a zip file containing the Openvpn configuration files 12 | - Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP 13 | - Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP 14 | 15 | If the list of servers requires to login **or** is hidden behind an interactive configurator, 16 | you can only use a custom Openvpn configuration file. 17 | [The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: docker 9 | directory: / 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: gomod 13 | directory: / 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/ci-skip.yml: -------------------------------------------------------------------------------- 1 | name: No trigger file paths 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - .github/workflows/ci.yml 8 | - cmd/** 9 | - internal/** 10 | - pkg/** 11 | - .dockerignore 12 | - .golangci.yml 13 | - Dockerfile 14 | - go.mod 15 | - go.sum 16 | pull_request: 17 | paths-ignore: 18 | - .github/workflows/ci.yml 19 | - cmd/** 20 | - internal/** 21 | - pkg/** 22 | - .dockerignore 23 | - .golangci.yml 24 | - Dockerfile 25 | - go.mod 26 | - go.sum 27 | 28 | jobs: 29 | verify: 30 | runs-on: ubuntu-latest 31 | permissions: 32 | actions: read 33 | steps: 34 | - name: No trigger path triggered for required verify workflow. 35 | run: exit 0 36 | -------------------------------------------------------------------------------- /.github/workflows/closed-issue.yml: -------------------------------------------------------------------------------- 1 | name: Closed issue 2 | on: 3 | issues: 4 | types: [closed] 5 | 6 | jobs: 7 | comment: 8 | permissions: 9 | issues: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: peter-evans/create-or-update-comment@v4 13 | with: 14 | token: ${{ github.token }} 15 | issue-number: ${{ github.event.issue.number }} 16 | body: | 17 | Closed issues are **NOT** monitored, so commenting here is likely to be not seen. 18 | If you think this is *still unresolved* and have **more information** to bring, please create another issue. 19 | 20 | This is an automated comment setup because @qdm12 is the sole maintainer of this project 21 | which became too popular to monitor issues closed. 22 | -------------------------------------------------------------------------------- /.github/workflows/configs/mlc-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^https://console.substack.com/p/console-72$" 5 | } 6 | ], 7 | "timeout": "20s", 8 | "retryOn429": false, 9 | "fallbackRetryDelay": "30s", 10 | "aliveStatusCodes": [ 11 | 200 12 | ] 13 | } -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: labels 2 | on: 3 | push: 4 | branches: [master] 5 | paths: 6 | - .github/labels.yml 7 | - .github/workflows/labels.yml 8 | jobs: 9 | labeler: 10 | permissions: 11 | issues: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: crazy-max/ghaction-github-labeler@v5 16 | with: 17 | yaml-file: .github/labels.yml 18 | -------------------------------------------------------------------------------- /.github/workflows/markdown-skip.yml: -------------------------------------------------------------------------------- 1 | name: Markdown 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - "**.md" 8 | - .github/workflows/markdown.yml 9 | pull_request: 10 | paths-ignore: 11 | - "**.md" 12 | - .github/workflows/markdown.yml 13 | 14 | jobs: 15 | markdown: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | steps: 20 | - name: No trigger path triggered for required markdown workflow. 21 | run: exit 0 22 | -------------------------------------------------------------------------------- /.github/workflows/opened-issue.yml: -------------------------------------------------------------------------------- 1 | name: Opened issue 2 | on: 3 | issues: 4 | types: [opened] 5 | 6 | jobs: 7 | comment: 8 | permissions: 9 | issues: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: peter-evans/create-or-update-comment@v4 13 | with: 14 | token: ${{ github.token }} 15 | issue-number: ${{ github.event.issue.number }} 16 | body: | 17 | @qdm12 is more or less the only maintainer of this project and works on it in his free time. 18 | Please: 19 | - **do not** ask for updates, be patient 20 | - :+1: the issue to show your support instead of commenting 21 | @qdm12 usually checks issues at least once a week, if this is a new urgent bug, 22 | [revert to an older tagged container image](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | scratch.txt 2 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false 3 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // This list should be kept to the strict minimum 3 | // to develop this project. 4 | "recommendations": [ 5 | "golang.go", 6 | "davidanson.vscode-markdownlint", 7 | ], 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Update a VPN provider servers data", 6 | "type": "go", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}", 9 | "program": "cmd/gluetun/main.go", 10 | "args": [ 11 | "update", 12 | "${input:updateMode}", 13 | "-providers", 14 | "${input:provider}" 15 | ], 16 | } 17 | ], 18 | "inputs": [ 19 | { 20 | "id": "provider", 21 | "type": "promptString", 22 | "description": "Please enter a provider (or comma separated list of providers)", 23 | }, 24 | { 25 | "id": "updateMode", 26 | "type": "pickString", 27 | "description": "Update mode to use", 28 | "options": [ 29 | "-maintainer", 30 | "-enduser" 31 | ], 32 | "default": "-maintainer" 33 | }, 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // The settings should be kept to the strict minimum 3 | // to develop this project. 4 | "files.eol": "\n", 5 | "editor.formatOnSave": true, 6 | "go.buildTags": "linux", 7 | "go.toolsEnvVars": { 8 | "CGO_ENABLED": "0" 9 | }, 10 | "go.testEnvVars": { 11 | "CGO_ENABLED": "1" 12 | }, 13 | "go.testFlags": [ 14 | "-v", 15 | "-race" 16 | ], 17 | "go.testTimeout": "10s", 18 | "go.coverOnSingleTest": true, 19 | "go.coverOnSingleTestFile": true, 20 | "go.coverOnTestPackage": true, 21 | "go.useLanguageServer": true, 22 | "[go]": { 23 | "editor.codeActionsOnSave": { 24 | "source.organizeImports": "explicit" 25 | } 26 | }, 27 | "go.lintTool": "golangci-lint", 28 | "go.lintOnSave": "package" 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Quentin McGaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /doc/logo_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qdm12/gluetun/9933dd3ec5c88d6a6b0f08a23031674c0591248c/doc/logo_256.png -------------------------------------------------------------------------------- /internal/alpine/alpine.go: -------------------------------------------------------------------------------- 1 | package alpine 2 | 3 | import ( 4 | "os/user" 5 | ) 6 | 7 | type Alpine struct { 8 | alpineReleasePath string 9 | passwdPath string 10 | lookupID func(uid string) (*user.User, error) 11 | lookup func(username string) (*user.User, error) 12 | } 13 | 14 | func New() *Alpine { 15 | return &Alpine{ 16 | alpineReleasePath: "/etc/alpine-release", 17 | passwdPath: "/etc/passwd", 18 | lookupID: user.LookupId, 19 | lookup: user.Lookup, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/alpine/version.go: -------------------------------------------------------------------------------- 1 | package alpine 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func (a *Alpine) Version(context.Context) (version string, err error) { 11 | file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0) 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | b, err := io.ReadAll(file) 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | if err := file.Close(); err != nil { 22 | return "", err 23 | } 24 | 25 | version = strings.ReplaceAll(string(b), "\n", "") 26 | return version, nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/cli/ci.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "context" 4 | 5 | func (c *CLI) CI(context.Context) error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /internal/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type CLI struct { 4 | repoServersPath string 5 | } 6 | 7 | func New() *CLI { 8 | return &CLI{ 9 | repoServersPath: "./internal/storage/servers.json", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /internal/cli/clientkey.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func (c *CLI) ClientKey(args []string) error { 12 | flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError) 13 | const openVPNClientKeyPath = "/gluetun/client.key" // TODO deduplicate? 14 | filepath := flagSet.String("path", openVPNClientKeyPath, "file path to the client.key file") 15 | if err := flagSet.Parse(args); err != nil { 16 | return err 17 | } 18 | file, err := os.OpenFile(*filepath, os.O_RDONLY, 0) 19 | if err != nil { 20 | return err 21 | } 22 | data, err := io.ReadAll(file) 23 | if err != nil { 24 | _ = file.Close() 25 | return err 26 | } 27 | if err := file.Close(); err != nil { 28 | return err 29 | } 30 | s := string(data) 31 | s = strings.ReplaceAll(s, "\n", "") 32 | s = strings.ReplaceAll(s, "\r", "") 33 | s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----") 34 | s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----") 35 | fmt.Println(s) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/cli/healthcheck.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/qdm12/gluetun/internal/configuration/settings" 10 | "github.com/qdm12/gluetun/internal/healthcheck" 11 | "github.com/qdm12/gosettings/reader" 12 | ) 13 | 14 | func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) { 15 | // Extract the health server port from the configuration. 16 | var config settings.Health 17 | err = config.Read(reader) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | config.SetDefaults() 23 | 24 | err = config.Validate() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | _, port, err := net.SplitHostPort(config.ServerAddress) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | const timeout = 10 * time.Second 35 | httpClient := &http.Client{Timeout: timeout} 36 | client := healthcheck.NewClient(httpClient) 37 | ctx, cancel := context.WithTimeout(ctx, timeout) 38 | defer cancel() 39 | 40 | url := "http://127.0.0.1:" + port 41 | return client.Check(ctx, url) 42 | } 43 | -------------------------------------------------------------------------------- /internal/cli/interfaces.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "github.com/qdm12/gluetun/internal/configuration/settings" 4 | 5 | type Source interface { 6 | Read() (settings settings.Settings, err error) 7 | ReadHealth() (health settings.Health, err error) 8 | String() string 9 | } 10 | -------------------------------------------------------------------------------- /internal/cli/nooplogger.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type noopLogger struct{} 4 | 5 | func newNoopLogger() *noopLogger { 6 | return new(noopLogger) 7 | } 8 | 9 | func (l *noopLogger) Info(string) {} 10 | -------------------------------------------------------------------------------- /internal/cli/warner.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type Warner interface { 4 | Warn(s string) 5 | } 6 | -------------------------------------------------------------------------------- /internal/command/cmder.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | // Cmder handles running subprograms synchronously and asynchronously. 4 | type Cmder struct{} 5 | 6 | func New() *Cmder { 7 | return &Cmder{} 8 | } 9 | -------------------------------------------------------------------------------- /internal/command/interfaces_local.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "io" 4 | 5 | type execCmd interface { 6 | CombinedOutput() ([]byte, error) 7 | StdoutPipe() (io.ReadCloser, error) 8 | StderrPipe() (io.ReadCloser, error) 9 | Start() error 10 | Wait() error 11 | } 12 | -------------------------------------------------------------------------------- /internal/command/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | //go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go 4 | -------------------------------------------------------------------------------- /internal/command/run.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | // Run runs a command in a blocking manner, returning its output and 9 | // an error if it failed. 10 | func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) { 11 | return run(cmd) 12 | } 13 | 14 | func run(cmd execCmd) (output string, err error) { 15 | stdout, err := cmd.CombinedOutput() 16 | output = string(stdout) 17 | output = strings.TrimSuffix(output, "\n") 18 | lines := stringToLines(output) 19 | for i := range lines { 20 | lines[i] = strings.TrimPrefix(lines[i], "'") 21 | lines[i] = strings.TrimSuffix(lines[i], "'") 22 | } 23 | output = strings.Join(lines, "\n") 24 | return output, err 25 | } 26 | 27 | func stringToLines(s string) (lines []string) { 28 | s = strings.TrimSuffix(s, "\n") 29 | return strings.Split(s, "\n") 30 | } 31 | -------------------------------------------------------------------------------- /internal/configuration/settings/deprecated.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "slices" 5 | 6 | "github.com/qdm12/gosettings/reader" 7 | "golang.org/x/exp/maps" 8 | ) 9 | 10 | func readObsolete(r *reader.Reader) (warnings []string) { 11 | keyToMessage := map[string]string{ 12 | "DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.", 13 | "DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.", 14 | "DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.", 15 | } 16 | sortedKeys := maps.Keys(keyToMessage) 17 | slices.Sort(sortedKeys) 18 | warnings = make([]string, 0, len(keyToMessage)) 19 | for _, key := range sortedKeys { 20 | if r.Get(key) != nil { 21 | warnings = append(warnings, keyToMessage[key]) 22 | } 23 | } 24 | return warnings 25 | } 26 | -------------------------------------------------------------------------------- /internal/configuration/settings/helpers.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | func ptrTo[T any](value T) *T { 4 | return &value 5 | } 6 | -------------------------------------------------------------------------------- /internal/configuration/settings/helpers/belong.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | func IsOneOf[T comparable](value T, choices ...T) (ok bool) { 4 | for _, choice := range choices { 5 | if value == choice { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /internal/configuration/settings/helpers_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import gomock "github.com/golang/mock/gomock" 4 | 5 | type sourceKeyValue struct { 6 | key string 7 | value string 8 | } 9 | 10 | func newMockSource(ctrl *gomock.Controller, keyValues []sourceKeyValue) *MockSource { 11 | source := NewMockSource(ctrl) 12 | var previousCall *gomock.Call 13 | for _, keyValue := range keyValues { 14 | transformedKey := keyValue.key 15 | keyTransformCall := source.EXPECT().KeyTransform(keyValue.key).Return(transformedKey) 16 | if previousCall != nil { 17 | keyTransformCall.After(previousCall) 18 | } 19 | isSet := keyValue.value != "" 20 | previousCall = source.EXPECT().Get(transformedKey). 21 | Return(keyValue.value, isSet).After(keyTransformCall) 22 | if isSet { 23 | previousCall = source.EXPECT().KeyTransform(keyValue.key). 24 | Return(transformedKey).After(previousCall) 25 | previousCall = source.EXPECT().String(). 26 | Return("mock source").After(previousCall) 27 | } 28 | } 29 | return source 30 | } 31 | -------------------------------------------------------------------------------- /internal/configuration/settings/interfaces.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type Warner interface { 4 | Warn(message string) 5 | } 6 | -------------------------------------------------------------------------------- /internal/configuration/settings/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | //go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Warner 4 | //go:generate mockgen -destination=mocks_reader_test.go -package=$GOPACKAGE github.com/qdm12/gosettings/reader Source 5 | -------------------------------------------------------------------------------- /internal/configuration/settings/openvpn_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_ivpnAccountID(t *testing.T) { 10 | t.Parallel() 11 | 12 | testCases := []struct { 13 | s string 14 | match bool 15 | }{ 16 | {}, 17 | {s: "abc"}, 18 | {s: "i"}, 19 | {s: "ivpn"}, 20 | {s: "ivpn-aaaa"}, 21 | {s: "ivpn-aaaa-aaaa"}, 22 | {s: "ivpn-aaaa-aaaa-aaa"}, 23 | {s: "ivpn-aaaa-aaaa-aaaa", match: true}, 24 | {s: "ivpn-aaaa-aaaa-aaaaa"}, 25 | {s: "ivpn-a6B7-fP91-Zh6Y", match: true}, 26 | {s: "i-aaaa"}, 27 | {s: "i-aaaa-aaaa"}, 28 | {s: "i-aaaa-aaaa-aaa"}, 29 | {s: "i-aaaa-aaaa-aaaa", match: true}, 30 | {s: "i-aaaa-aaaa-aaaaa"}, 31 | {s: "i-a6B7-fP91-Zh6Y", match: true}, 32 | } 33 | 34 | for _, testCase := range testCases { 35 | t.Run(testCase.s, func(t *testing.T) { 36 | t.Parallel() 37 | 38 | match := ivpnAccountID.MatchString(testCase.s) 39 | 40 | assert.Equal(t, testCase.match, match) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/configuration/settings/portforward_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_PortForwarding_String(t *testing.T) { 10 | t.Parallel() 11 | 12 | settings := PortForwarding{ 13 | Enabled: ptrTo(false), 14 | } 15 | 16 | s := settings.String() 17 | 18 | assert.Empty(t, s) 19 | } 20 | -------------------------------------------------------------------------------- /internal/configuration/settings/validation/surfshark.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/surfshark/servers" 5 | ) 6 | 7 | // TODO remove in v4. 8 | func SurfsharkRetroLocChoices() (choices []string) { 9 | locationData := servers.LocationData() 10 | choices = make([]string, 0, len(locationData)) 11 | seen := make(map[string]struct{}, len(locationData)) 12 | for _, data := range locationData { 13 | if data.RetroLoc == "" { 14 | continue 15 | } 16 | if _, ok := seen[data.RetroLoc]; ok { 17 | continue 18 | } 19 | seen[data.RetroLoc] = struct{}{} 20 | choices = sortedInsert(choices, data.RetroLoc) 21 | } 22 | 23 | return choices 24 | } 25 | -------------------------------------------------------------------------------- /internal/configuration/sources/files/interfaces.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | type Warner interface { 4 | Warnf(format string, a ...interface{}) 5 | } 6 | -------------------------------------------------------------------------------- /internal/configuration/sources/secrets/helpers.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | func strPtrToStringIsSet(ptr *string) (s string, isSet bool) { 4 | if ptr == nil { 5 | return "", false 6 | } 7 | return *ptr, true 8 | } 9 | -------------------------------------------------------------------------------- /internal/configuration/sources/secrets/interfaces.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | type Warner interface { 4 | Warnf(format string, a ...interface{}) 5 | } 6 | -------------------------------------------------------------------------------- /internal/configuration/sources/secrets/wireguard.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/qdm12/gluetun/internal/configuration/sources/files" 8 | ) 9 | 10 | func (s *Source) lazyLoadWireguardConf() files.WireguardConfig { 11 | if s.cached.wireguardLoaded { 12 | return s.cached.wireguardConf 13 | } 14 | 15 | path := os.Getenv("WIREGUARD_CONF_SECRETFILE") 16 | if path == "" { 17 | path = filepath.Join(s.rootDirectory, "wg0.conf") 18 | } 19 | 20 | s.cached.wireguardLoaded = true 21 | var err error 22 | s.cached.wireguardConf, err = files.ParseWireguardConf(path) 23 | if err != nil { 24 | s.warner.Warnf("skipping Wireguard config: %s", err) 25 | } 26 | return s.cached.wireguardConf 27 | } 28 | -------------------------------------------------------------------------------- /internal/constants/colors.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "github.com/fatih/color" 4 | 5 | func ColorOpenvpn() *color.Color { 6 | return color.New(color.FgHiMagenta) 7 | } 8 | -------------------------------------------------------------------------------- /internal/constants/openvpn/auth.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | const ( 4 | SHA1 = "sha1" 5 | SHA256 = "sha256" 6 | SHA512 = "sha512" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/openvpn/ciphers.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | const ( 4 | AES128cbc = "aes-128-cbc" 5 | AES192cbc = "aes-192-cbc" 6 | AES256cbc = "aes-256-cbc" 7 | AES128gcm = "aes-128-gcm" 8 | AES192gcm = "aes-192-gcm" 9 | AES256gcm = "aes-256-gcm" 10 | Chacha20Poly1305 = "chacha20-poly1305" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/constants/openvpn/paths.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | const ( 4 | // AuthConf is the file path to the OpenVPN auth file. 5 | AuthConf = "/etc/openvpn/auth.conf" 6 | // AskPassPath is the file path to the decryption passphrase for 7 | // and encrypted private key, which is pointed by `askpass`. 8 | AskPassPath = "/etc/openvpn/askpass" //nolint:gosec 9 | ) 10 | -------------------------------------------------------------------------------- /internal/constants/openvpn/versions.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | const ( 4 | Openvpn25 = "2.5" 5 | Openvpn26 = "2.6" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/constants/paths.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // ServersData is the server information filepath. 5 | ServersData = "/gluetun/servers.json" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/constants/protocol.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // TCP is a network protocol (reliable and slower than UDP). 5 | TCP string = "tcp" 6 | // UDP is a network protocol (unreliable and faster than TCP). 7 | UDP string = "udp" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/constants/providers/providers_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_All(t *testing.T) { 10 | t.Parallel() 11 | 12 | all := All() 13 | assert.NotContains(t, all, Custom) 14 | assert.NotEmpty(t, all) 15 | } 16 | 17 | func Test_AllWithCustom(t *testing.T) { 18 | t.Parallel() 19 | 20 | all := AllWithCustom() 21 | assert.Contains(t, all, Custom) 22 | assert.Len(t, all, len(All())+1) 23 | } 24 | -------------------------------------------------------------------------------- /internal/constants/status.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/models" 5 | ) 6 | 7 | const ( 8 | Starting models.LoopStatus = "starting" 9 | Running models.LoopStatus = "running" 10 | Stopping models.LoopStatus = "stopping" 11 | Stopped models.LoopStatus = "stopped" 12 | Crashed models.LoopStatus = "crashed" 13 | Completed models.LoopStatus = "completed" 14 | ) 15 | -------------------------------------------------------------------------------- /internal/constants/vpn/protocol.go: -------------------------------------------------------------------------------- 1 | package vpn 2 | 3 | const ( 4 | OpenVPN = "openvpn" 5 | Wireguard = "wireguard" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/dns/logger.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | type Logger interface { 4 | Debug(s string) 5 | Info(s string) 6 | Warn(s string) 7 | Error(s string) 8 | } 9 | -------------------------------------------------------------------------------- /internal/dns/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/qdm12/gluetun/internal/configuration/settings" 8 | "github.com/qdm12/gluetun/internal/models" 9 | ) 10 | 11 | func New(statusApplier StatusApplier, 12 | settings settings.DNS, 13 | updateTicker chan<- struct{}, 14 | ) *State { 15 | return &State{ 16 | statusApplier: statusApplier, 17 | settings: settings, 18 | updateTicker: updateTicker, 19 | } 20 | } 21 | 22 | type State struct { 23 | statusApplier StatusApplier 24 | 25 | settings settings.DNS 26 | settingsMu sync.RWMutex 27 | 28 | updateTicker chan<- struct{} 29 | } 30 | 31 | type StatusApplier interface { 32 | ApplyStatus(ctx context.Context, status models.LoopStatus) ( 33 | outcome string, err error) 34 | } 35 | -------------------------------------------------------------------------------- /internal/dns/status.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | func (l *Loop) GetStatus() (status models.LoopStatus) { 10 | return l.statusManager.GetStatus() 11 | } 12 | 13 | func (l *Loop) ApplyStatus(ctx context.Context, status models.LoopStatus) ( 14 | outcome string, err error, 15 | ) { 16 | return l.statusManager.ApplyStatus(ctx, status) 17 | } 18 | -------------------------------------------------------------------------------- /internal/firewall/interfaces.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import "os/exec" 4 | 5 | type CmdRunner interface { 6 | Run(cmd *exec.Cmd) (output string, err error) 7 | } 8 | 9 | type Logger interface { 10 | Debug(s string) 11 | Info(s string) 12 | Warn(s string) 13 | Error(s string) 14 | } 15 | -------------------------------------------------------------------------------- /internal/firewall/iptablesmix.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func (c *Config) runMixedIptablesInstructions(ctx context.Context, instructions []string) error { 8 | for _, instruction := range instructions { 9 | if err := c.runMixedIptablesInstruction(ctx, instruction); err != nil { 10 | return err 11 | } 12 | } 13 | return nil 14 | } 15 | 16 | func (c *Config) runMixedIptablesInstruction(ctx context.Context, instruction string) error { 17 | if err := c.runIptablesInstruction(ctx, instruction); err != nil { 18 | return err 19 | } 20 | return c.runIP6tablesInstruction(ctx, instruction) 21 | } 22 | -------------------------------------------------------------------------------- /internal/firewall/logger.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | ) 7 | 8 | func (c *Config) logIgnoredSubnetFamily(subnet netip.Prefix) { 9 | c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+ 10 | "no default route matching its family", subnet)) 11 | } 12 | -------------------------------------------------------------------------------- /internal/firewall/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | //go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . CmdRunner,Logger 4 | -------------------------------------------------------------------------------- /internal/format/duration.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // FriendlyDuration formats a duration in an approximate, human friendly duration. 9 | // For example 55 hours will result in "2 days". 10 | func FriendlyDuration(duration time.Duration) string { 11 | const twoDays = 48 * time.Hour 12 | switch { 13 | case duration < time.Minute: 14 | seconds := int(duration.Round(time.Second).Seconds()) 15 | const two = 2 16 | if seconds < two { 17 | return fmt.Sprintf("%d second", seconds) 18 | } 19 | return fmt.Sprintf("%d seconds", seconds) 20 | case duration <= time.Hour: 21 | minutes := int(duration.Round(time.Minute).Minutes()) 22 | if minutes == 1 { 23 | return "1 minute" 24 | } 25 | return fmt.Sprintf("%d minutes", minutes) 26 | case duration < twoDays: 27 | hours := int(duration.Truncate(time.Hour).Hours()) 28 | return fmt.Sprintf("%d hours", hours) 29 | default: 30 | const hoursInDay = 24 31 | days := int(duration.Truncate(time.Hour).Hours() / hoursInDay) 32 | return fmt.Sprintf("%d days", days) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/healthcheck/client.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | var ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK") 12 | 13 | type Client struct { 14 | httpClient *http.Client 15 | } 16 | 17 | func NewClient(httpClient *http.Client) *Client { 18 | return &Client{ 19 | httpClient: httpClient, 20 | } 21 | } 22 | 23 | func (c *Client) Check(ctx context.Context, url string) error { 24 | request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 25 | if err != nil { 26 | return err 27 | } 28 | response, err := c.httpClient.Do(request) 29 | if err != nil { 30 | return err 31 | } 32 | defer response.Body.Close() 33 | if response.StatusCode == http.StatusOK { 34 | return nil 35 | } 36 | b, err := io.ReadAll(response.Body) 37 | if err != nil { 38 | return err 39 | } 40 | return fmt.Errorf("%w: %d %s: %s", ErrHTTPStatusNotOK, 41 | response.StatusCode, response.Status, string(b)) 42 | } 43 | -------------------------------------------------------------------------------- /internal/healthcheck/handler.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "sync" 7 | ) 8 | 9 | type handler struct { 10 | healthErr error 11 | healthErrMu sync.RWMutex 12 | } 13 | 14 | var errHealthcheckNotRunYet = errors.New("healthcheck did not run yet") 15 | 16 | func newHandler() *handler { 17 | return &handler{ 18 | healthErr: errHealthcheckNotRunYet, 19 | } 20 | } 21 | 22 | func (h *handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { 23 | if request.Method != http.MethodGet { 24 | http.Error(responseWriter, "method not supported for healthcheck", http.StatusBadRequest) 25 | return 26 | } 27 | if err := h.getErr(); err != nil { 28 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 29 | return 30 | } 31 | responseWriter.WriteHeader(http.StatusOK) 32 | } 33 | 34 | func (h *handler) setErr(err error) { 35 | h.healthErrMu.Lock() 36 | defer h.healthErrMu.Unlock() 37 | h.healthErr = err 38 | } 39 | 40 | func (h *handler) getErr() (err error) { 41 | h.healthErrMu.RLock() 42 | defer h.healthErrMu.RUnlock() 43 | return h.healthErr 44 | } 45 | -------------------------------------------------------------------------------- /internal/healthcheck/logger.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | type Logger interface { 4 | Debug(s string) 5 | Info(s string) 6 | Error(s string) 7 | } 8 | -------------------------------------------------------------------------------- /internal/healthcheck/openvpn.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/qdm12/gluetun/internal/constants" 8 | ) 9 | 10 | type vpnHealth struct { 11 | loop StatusApplier 12 | healthyWait time.Duration 13 | healthyTimer *time.Timer 14 | } 15 | 16 | func (s *Server) onUnhealthyVPN(ctx context.Context, lastErrMessage string) { 17 | s.logger.Info("program has been unhealthy for " + 18 | s.vpn.healthyWait.String() + ": restarting VPN (healthcheck error: " + lastErrMessage + ")") 19 | s.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md") 20 | s.logger.Info("DO NOT OPEN AN ISSUE UNLESS YOU READ AND TRIED EACH POSSIBLE SOLUTION") 21 | _, _ = s.vpn.loop.ApplyStatus(ctx, constants.Stopped) 22 | _, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running) 23 | s.vpn.healthyWait += *s.config.VPN.Addition 24 | s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait) 25 | } 26 | -------------------------------------------------------------------------------- /internal/healthcheck/server.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/qdm12/gluetun/internal/configuration/settings" 8 | "github.com/qdm12/gluetun/internal/models" 9 | ) 10 | 11 | type Server struct { 12 | logger Logger 13 | handler *handler 14 | dialer *net.Dialer 15 | config settings.Health 16 | vpn vpnHealth 17 | } 18 | 19 | func NewServer(config settings.Health, 20 | logger Logger, vpnLoop StatusApplier, 21 | ) *Server { 22 | return &Server{ 23 | logger: logger, 24 | handler: newHandler(), 25 | dialer: &net.Dialer{ 26 | Resolver: &net.Resolver{ 27 | PreferGo: true, 28 | }, 29 | }, 30 | config: config, 31 | vpn: vpnHealth{ 32 | loop: vpnLoop, 33 | healthyWait: *config.VPN.Initial, 34 | }, 35 | } 36 | } 37 | 38 | type StatusApplier interface { 39 | ApplyStatus(ctx context.Context, status models.LoopStatus) ( 40 | outcome string, err error) 41 | } 42 | -------------------------------------------------------------------------------- /internal/httpproxy/accept.go: -------------------------------------------------------------------------------- 1 | package httpproxy 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func (h *handler) isAccepted(responseWriter http.ResponseWriter, request *http.Request) bool { 9 | // Not compatible with HTTP < 1.0 or HTTP >= 2.0 (see https://github.com/golang/go/issues/14797#issuecomment-196103814) 10 | const ( 11 | minimalMajorVersion = 1 12 | minimalMinorVersion = 0 13 | maximumMajorVersion = 2 14 | maximumMinorVersion = 0 15 | ) 16 | if !request.ProtoAtLeast(minimalMajorVersion, minimalMinorVersion) || 17 | request.ProtoAtLeast(maximumMajorVersion, maximumMinorVersion) { 18 | message := fmt.Sprintf("http version not supported: %s", request.Proto) 19 | h.logger.Info(message + ", from " + request.RemoteAddr) 20 | http.Error(responseWriter, message, http.StatusBadRequest) 21 | return false 22 | } 23 | return true 24 | } 25 | -------------------------------------------------------------------------------- /internal/httpproxy/handler_test.go: -------------------------------------------------------------------------------- 1 | package httpproxy 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_returnRedirect(t *testing.T) { 11 | t.Parallel() 12 | 13 | err := returnRedirect(nil, nil) 14 | 15 | assert.Equal(t, http.ErrUseLastResponse, err) 16 | } 17 | -------------------------------------------------------------------------------- /internal/httpproxy/logger.go: -------------------------------------------------------------------------------- 1 | package httpproxy 2 | 3 | type Logger interface { 4 | infoErrorer 5 | Debug(s string) 6 | Warn(s string) 7 | } 8 | 9 | type infoErrorer interface { 10 | Info(s string) 11 | Error(s string) 12 | } 13 | -------------------------------------------------------------------------------- /internal/httpproxy/settings.go: -------------------------------------------------------------------------------- 1 | package httpproxy 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/configuration/settings" 7 | ) 8 | 9 | func (l *Loop) GetSettings() (settings settings.HTTPProxy) { 10 | return l.state.GetSettings() 11 | } 12 | 13 | func (l *Loop) SetSettings(ctx context.Context, settings settings.HTTPProxy) ( 14 | outcome string, 15 | ) { 16 | return l.state.SetSettings(ctx, settings) 17 | } 18 | -------------------------------------------------------------------------------- /internal/httpproxy/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/qdm12/gluetun/internal/configuration/settings" 8 | "github.com/qdm12/gluetun/internal/models" 9 | ) 10 | 11 | func New(statusApplier StatusApplier, 12 | settings settings.HTTPProxy, 13 | ) *State { 14 | return &State{ 15 | statusApplier: statusApplier, 16 | settings: settings, 17 | } 18 | } 19 | 20 | type State struct { 21 | statusApplier StatusApplier 22 | settings settings.HTTPProxy 23 | settingsMu sync.RWMutex 24 | } 25 | 26 | type StatusApplier interface { 27 | ApplyStatus(ctx context.Context, status models.LoopStatus) ( 28 | outcome string, err error) 29 | } 30 | -------------------------------------------------------------------------------- /internal/httpproxy/status.go: -------------------------------------------------------------------------------- 1 | package httpproxy 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | func (l *Loop) GetStatus() (status models.LoopStatus) { 10 | return l.statusManager.GetStatus() 11 | } 12 | 13 | func (l *Loop) ApplyStatus(ctx context.Context, status models.LoopStatus) ( 14 | outcome string, err error, 15 | ) { 16 | return l.statusManager.ApplyStatus(ctx, status) 17 | } 18 | -------------------------------------------------------------------------------- /internal/httpserver/address.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | // GetAddress obtains the address the HTTP server is listening on. 4 | func (s *Server) GetAddress() (address string) { 5 | <-s.addressSet 6 | return s.address 7 | } 8 | -------------------------------------------------------------------------------- /internal/httpserver/helpers_test.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "regexp" 5 | 6 | gomock "github.com/golang/mock/gomock" 7 | ) 8 | 9 | var _ Logger = (*testLogger)(nil) 10 | 11 | type testLogger struct{} 12 | 13 | func (t *testLogger) Info(string) {} 14 | func (t *testLogger) Warn(string) {} 15 | func (t *testLogger) Error(string) {} 16 | 17 | var _ gomock.Matcher = (*regexMatcher)(nil) 18 | 19 | type regexMatcher struct { 20 | regexp *regexp.Regexp 21 | } 22 | 23 | func (r *regexMatcher) Matches(x interface{}) bool { 24 | s, ok := x.(string) 25 | if !ok { 26 | return false 27 | } 28 | return r.regexp.MatchString(s) 29 | } 30 | 31 | func (r *regexMatcher) String() string { 32 | return "regular expression " + r.regexp.String() 33 | } 34 | 35 | func newRegexMatcher(regex string) *regexMatcher { 36 | return ®exMatcher{ 37 | regexp: regexp.MustCompile(regex), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/httpserver/logger.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | // Logger is the logger interface accepted by the 4 | // HTTP server. 5 | type Logger interface { 6 | Info(msg string) 7 | Warn(msg string) 8 | Error(msg string) 9 | } 10 | -------------------------------------------------------------------------------- /internal/loopstate/get.go: -------------------------------------------------------------------------------- 1 | package loopstate 2 | 3 | import "github.com/qdm12/gluetun/internal/models" 4 | 5 | // GetStatus gets the status thread safely. 6 | func (s *State) GetStatus() (status models.LoopStatus) { 7 | s.statusMu.RLock() 8 | defer s.statusMu.RUnlock() 9 | return s.status 10 | } 11 | -------------------------------------------------------------------------------- /internal/loopstate/lock.go: -------------------------------------------------------------------------------- 1 | package loopstate 2 | 3 | func (s *State) Lock() { s.loopMu.Lock() } 4 | func (s *State) Unlock() { s.loopMu.Unlock() } 5 | -------------------------------------------------------------------------------- /internal/loopstate/set.go: -------------------------------------------------------------------------------- 1 | package loopstate 2 | 3 | import "github.com/qdm12/gluetun/internal/models" 4 | 5 | // SetStatus sets the status thread safely. 6 | // It should only be called by the loop internal code since 7 | // it does not interact with the loop code directly. 8 | func (s *State) SetStatus(status models.LoopStatus) { 9 | s.statusMu.Lock() 10 | defer s.statusMu.Unlock() 11 | s.status = status 12 | } 13 | -------------------------------------------------------------------------------- /internal/loopstate/state.go: -------------------------------------------------------------------------------- 1 | package loopstate 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | func New(status models.LoopStatus, 10 | start chan<- struct{}, running <-chan models.LoopStatus, 11 | stop chan<- struct{}, stopped <-chan struct{}, 12 | ) *State { 13 | return &State{ 14 | status: status, 15 | start: start, 16 | running: running, 17 | stop: stop, 18 | stopped: stopped, 19 | } 20 | } 21 | 22 | type State struct { 23 | loopMu sync.RWMutex 24 | 25 | status models.LoopStatus 26 | statusMu sync.RWMutex 27 | 28 | start chan<- struct{} 29 | running <-chan models.LoopStatus 30 | stop chan<- struct{} 31 | stopped <-chan struct{} 32 | } 33 | -------------------------------------------------------------------------------- /internal/mod/probe.go: -------------------------------------------------------------------------------- 1 | package mod 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Probe loads the given kernel module and its dependencies. 8 | func Probe(moduleName string) error { 9 | modulesInfo, err := getModulesInfo() 10 | if err != nil { 11 | return fmt.Errorf("getting modules information: %w", err) 12 | } 13 | 14 | modulePath, err := findModulePath(moduleName, modulesInfo) 15 | if err != nil { 16 | return fmt.Errorf("finding module path: %w", err) 17 | } 18 | 19 | info := modulesInfo[modulePath] 20 | if info.state == builtin || info.state == loaded { 21 | return nil 22 | } 23 | 24 | info.state = loading 25 | for _, dependencyModulePath := range info.dependencyPaths { 26 | err = initDependencies(dependencyModulePath, modulesInfo) 27 | if err != nil { 28 | return fmt.Errorf("init dependencies: %w", err) 29 | } 30 | } 31 | 32 | err = initModule(modulePath) 33 | if err != nil { 34 | return fmt.Errorf("init module: %w", err) 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/models/alias.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | // LoopStatus status such as stopped or running. 5 | LoopStatus string 6 | ) 7 | 8 | func (ls LoopStatus) String() string { 9 | return string(ls) 10 | } 11 | -------------------------------------------------------------------------------- /internal/models/build.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type BuildInformation struct { 4 | Version string `json:"version"` 5 | Commit string `json:"commit"` 6 | Created string `json:"created"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/models/filters.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type FilterChoices struct { 4 | Countries []string 5 | Regions []string 6 | Cities []string 7 | Categories []string 8 | ISPs []string 9 | Names []string 10 | Hostnames []string 11 | } 12 | -------------------------------------------------------------------------------- /internal/models/publicip.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "net/netip" 5 | ) 6 | 7 | type PublicIP struct { 8 | IP netip.Addr `json:"public_ip,omitempty"` 9 | Region string `json:"region,omitempty"` 10 | Country string `json:"country,omitempty"` 11 | City string `json:"city,omitempty"` 12 | Hostname string `json:"hostname,omitempty"` 13 | Location string `json:"location,omitempty"` 14 | Organization string `json:"organization,omitempty"` 15 | PostalCode string `json:"postal_code,omitempty"` 16 | Timezone string `json:"timezone,omitempty"` 17 | } 18 | 19 | func (p *PublicIP) Copy() (publicIPCopy PublicIP) { 20 | publicIPCopy = PublicIP{ 21 | IP: p.IP, 22 | Region: p.Region, 23 | Country: p.Country, 24 | City: p.City, 25 | Hostname: p.Hostname, 26 | Location: p.Location, 27 | Organization: p.Organization, 28 | PostalCode: p.PostalCode, 29 | Timezone: p.Timezone, 30 | } 31 | return publicIPCopy 32 | } 33 | -------------------------------------------------------------------------------- /internal/models/sort.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "sort" 4 | 5 | var _ sort.Interface = (*SortableServers)(nil) 6 | 7 | type SortableServers []Server 8 | 9 | func (s SortableServers) Len() int { 10 | return len(s) 11 | } 12 | 13 | func (s SortableServers) Swap(i, j int) { 14 | s[i], s[j] = s[j], s[i] 15 | } 16 | 17 | func (s SortableServers) Less(i, j int) bool { 18 | a, b := s[i], s[j] 19 | 20 | if a.Country == b.Country { //nolint:nestif 21 | if a.Region == b.Region { 22 | if a.City == b.City { 23 | if a.ServerName == b.ServerName { 24 | if a.Number == b.Number { 25 | if a.Hostname == b.Hostname { 26 | if a.ISP == b.ISP { 27 | return a.VPN < b.VPN 28 | } 29 | return a.ISP < b.ISP 30 | } 31 | return a.Hostname < b.Hostname 32 | } 33 | return a.Number < b.Number 34 | } 35 | return a.ServerName < b.ServerName 36 | } 37 | return a.City < b.City 38 | } 39 | return a.Region < b.Region 40 | } 41 | return a.Country < b.Country 42 | } 43 | -------------------------------------------------------------------------------- /internal/natpmp/externaladdress.go: -------------------------------------------------------------------------------- 1 | package natpmp 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "net/netip" 8 | "time" 9 | ) 10 | 11 | // ExternalAddress fetches the duration since the start of epoch and the external 12 | // IPv4 address of the gateway. 13 | // See https://www.ietf.org/rfc/rfc6886.html#section-3.2 14 | func (c *Client) ExternalAddress(ctx context.Context, gateway netip.Addr) ( 15 | durationSinceStartOfEpoch time.Duration, 16 | externalIPv4Address netip.Addr, err error, 17 | ) { 18 | request := []byte{0, 0} // version 0, operationCode 0 19 | const responseSize = 12 20 | response, err := c.rpc(ctx, gateway, request, responseSize) 21 | if err != nil { 22 | return 0, externalIPv4Address, fmt.Errorf("executing remote procedure call: %w", err) 23 | } 24 | 25 | secondsSinceStartOfEpoch := binary.BigEndian.Uint32(response[4:8]) 26 | durationSinceStartOfEpoch = time.Duration(secondsSinceStartOfEpoch) * time.Second 27 | externalIPv4Address = netip.AddrFrom4([4]byte{response[8], response[9], response[10], response[11]}) 28 | return durationSinceStartOfEpoch, externalIPv4Address, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/natpmp/natpmp.go: -------------------------------------------------------------------------------- 1 | package natpmp 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Client is a NAT-PMP protocol client. 8 | type Client struct { 9 | serverPort uint16 10 | initialConnectionDuration time.Duration 11 | maxRetries uint 12 | } 13 | 14 | // New creates a new NAT-PMP client. 15 | func New() (client *Client) { 16 | const natpmpPort = 5351 17 | 18 | // Parameters described in https://www.ietf.org/rfc/rfc6886.html#section-3.1 19 | const initialConnectionDuration = 250 * time.Millisecond 20 | const maxTries = 9 // 64 seconds 21 | return &Client{ 22 | serverPort: natpmpPort, 23 | initialConnectionDuration: initialConnectionDuration, 24 | maxRetries: maxTries, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/natpmp/natpmp_test.go: -------------------------------------------------------------------------------- 1 | package natpmp 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_New(t *testing.T) { 11 | t.Parallel() 12 | 13 | expectedClient := &Client{ 14 | serverPort: 5351, 15 | initialConnectionDuration: 250 * time.Millisecond, 16 | maxRetries: 9, 17 | } 18 | client := New() 19 | assert.Equal(t, expectedClient, client) 20 | } 21 | -------------------------------------------------------------------------------- /internal/netlink/address.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package netlink 4 | 5 | import ( 6 | "github.com/vishvananda/netlink" 7 | ) 8 | 9 | func (n *NetLink) AddrList(link Link, family int) ( 10 | addresses []Addr, err error, 11 | ) { 12 | netlinkLink := linkToNetlinkLink(&link) 13 | netlinkAddresses, err := netlink.AddrList(netlinkLink, family) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | addresses = make([]Addr, len(netlinkAddresses)) 19 | for i := range netlinkAddresses { 20 | addresses[i].Network = netIPNetToNetipPrefix(netlinkAddresses[i].IPNet) 21 | } 22 | 23 | return addresses, nil 24 | } 25 | 26 | func (n *NetLink) AddrReplace(link Link, addr Addr) error { 27 | netlinkLink := linkToNetlinkLink(&link) 28 | netlinkAddress := netlink.Addr{ 29 | IPNet: netipPrefixToIPNet(addr.Network), 30 | } 31 | 32 | return netlink.AddrReplace(netlinkLink, &netlinkAddress) 33 | } 34 | -------------------------------------------------------------------------------- /internal/netlink/address_unspecified.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin 2 | 3 | package netlink 4 | 5 | func (n *NetLink) AddrList(link Link, family int) ( 6 | addresses []Addr, err error, 7 | ) { 8 | panic("not implemented") 9 | } 10 | 11 | func (n *NetLink) AddrReplace(Link, Addr) error { 12 | panic("not implemented") 13 | } 14 | -------------------------------------------------------------------------------- /internal/netlink/family.go: -------------------------------------------------------------------------------- 1 | package netlink 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | FamilyAll = 0 9 | FamilyV4 = 2 10 | FamilyV6 = 10 11 | ) 12 | 13 | func FamilyToString(family int) string { 14 | switch family { 15 | case FamilyAll: 16 | return "all" //nolint:goconst 17 | case FamilyV4: 18 | return "v4" 19 | case FamilyV6: 20 | return "v6" 21 | default: 22 | return fmt.Sprint(family) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/netlink/helpers_test.go: -------------------------------------------------------------------------------- 1 | package netlink 2 | 3 | import "net/netip" 4 | 5 | func makeNetipPrefix(n byte) netip.Prefix { 6 | const bits = 24 7 | return netip.PrefixFrom(netip.AddrFrom4([4]byte{n, n, n, 0}), bits) 8 | } 9 | -------------------------------------------------------------------------------- /internal/netlink/interfaces.go: -------------------------------------------------------------------------------- 1 | package netlink 2 | 3 | import "github.com/qdm12/log" 4 | 5 | type DebugLogger interface { 6 | Debug(message string) 7 | Debugf(format string, args ...any) 8 | Patch(options ...log.Option) 9 | } 10 | -------------------------------------------------------------------------------- /internal/netlink/link_unspecified.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin 2 | 3 | package netlink 4 | 5 | func (n *NetLink) LinkList() (links []Link, err error) { 6 | panic("not implemented") 7 | } 8 | 9 | func (n *NetLink) LinkByName(name string) (link Link, err error) { 10 | panic("not implemented") 11 | } 12 | 13 | func (n *NetLink) LinkByIndex(index int) (link Link, err error) { 14 | panic("not implemented") 15 | } 16 | 17 | func (n *NetLink) LinkAdd(link Link) (linkIndex int, err error) { 18 | panic("not implemented") 19 | } 20 | 21 | func (n *NetLink) LinkDel(link Link) (err error) { 22 | panic("not implemented") 23 | } 24 | 25 | func (n *NetLink) LinkSetUp(link Link) (linkIndex int, err error) { 26 | panic("not implemented") 27 | } 28 | 29 | func (n *NetLink) LinkSetDown(link Link) (err error) { 30 | panic("not implemented") 31 | } 32 | -------------------------------------------------------------------------------- /internal/netlink/netlink.go: -------------------------------------------------------------------------------- 1 | package netlink 2 | 3 | import "github.com/qdm12/log" 4 | 5 | type NetLink struct { 6 | debugLogger DebugLogger 7 | } 8 | 9 | func New(debugLogger DebugLogger) *NetLink { 10 | return &NetLink{ 11 | debugLogger: debugLogger, 12 | } 13 | } 14 | 15 | func (n *NetLink) PatchLoggerLevel(level log.Level) { 16 | n.debugLogger.Patch(log.SetLevel(level)) 17 | } 18 | -------------------------------------------------------------------------------- /internal/netlink/route_unspecified.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin 2 | 3 | package netlink 4 | 5 | func (n *NetLink) RouteList(family int) ( 6 | routes []Route, err error, 7 | ) { 8 | panic("not implemented") 9 | } 10 | 11 | func (n *NetLink) RouteAdd(route Route) error { 12 | panic("not implemented") 13 | } 14 | 15 | func (n *NetLink) RouteDel(route Route) error { 16 | panic("not implemented") 17 | } 18 | 19 | func (n *NetLink) RouteReplace(route Route) error { 20 | panic("not implemented") 21 | } 22 | -------------------------------------------------------------------------------- /internal/netlink/rule_unspecified.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package netlink 4 | 5 | func NewRule() Rule { 6 | return Rule{} 7 | } 8 | 9 | func (n *NetLink) RuleList(family int) (rules []Rule, err error) { 10 | panic("not implemented") 11 | } 12 | 13 | func (n *NetLink) RuleAdd(rule Rule) error { 14 | panic("not implemented") 15 | } 16 | 17 | func (n *NetLink) RuleDel(rule Rule) error { 18 | panic("not implemented") 19 | } 20 | -------------------------------------------------------------------------------- /internal/netlink/wireguard_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package netlink 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_NetLink_IsWireguardSupported(t *testing.T) { 12 | t.Skip() // TODO unskip once the data race problem with netlink.GenlFamilyList() is fixed 13 | 14 | t.Parallel() 15 | netLink := &NetLink{} 16 | ok, err := netLink.IsWireguardSupported() 17 | require.NoError(t, err) 18 | if ok { // cannot assert since this depends on kernel 19 | t.Log("wireguard is supported") 20 | } else { 21 | t.Log("wireguard is not supported") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/netlink/wireguard_unspecified.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package netlink 4 | 5 | func (n *NetLink) IsWireguardSupported() (ok bool, err error) { 6 | panic("not implemented") 7 | } 8 | -------------------------------------------------------------------------------- /internal/openvpn/config.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | func (c *Configurator) WriteConfig(lines []string) error { 9 | const permission = 0o644 10 | file, err := os.OpenFile(c.configPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, permission) 11 | if err != nil { 12 | return err 13 | } 14 | _, err = file.WriteString(strings.Join(lines, "\n")) 15 | if err != nil { 16 | _ = file.Close() 17 | return err 18 | } 19 | 20 | err = file.Chown(c.puid, c.pgid) 21 | if err != nil { 22 | _ = file.Close() 23 | return err 24 | } 25 | 26 | return file.Close() 27 | } 28 | -------------------------------------------------------------------------------- /internal/openvpn/extract/data.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/qdm12/gluetun/internal/models" 8 | ) 9 | 10 | var ( 11 | ErrRead = errors.New("cannot read file") 12 | ErrExtractConnection = errors.New("cannot extract connection from file") 13 | ) 14 | 15 | // Data extracts the lines and connection from the OpenVPN configuration file. 16 | func (e *Extractor) Data(filepath string) (lines []string, 17 | connection models.Connection, err error, 18 | ) { 19 | lines, err = readCustomConfigLines(filepath) 20 | if err != nil { 21 | return nil, connection, fmt.Errorf("reading configuration file: %w", err) 22 | } 23 | 24 | connection, err = extractDataFromLines(lines) 25 | if err != nil { 26 | return nil, connection, fmt.Errorf("extracting connection from file: %w", err) 27 | } 28 | 29 | return lines, connection, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/openvpn/extract/extractor.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | type Extractor struct{} 4 | 5 | func New() *Extractor { 6 | return new(Extractor) 7 | } 8 | -------------------------------------------------------------------------------- /internal/openvpn/extract/helpers_test.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func removeFile(t *testing.T, filename string) { 11 | t.Helper() 12 | err := os.RemoveAll(filename) 13 | require.NoError(t, err) 14 | } 15 | -------------------------------------------------------------------------------- /internal/openvpn/extract/pem.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/pem" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | var errPEMDecode = errors.New("cannot decode PEM encoded block") 11 | 12 | func PEM(b []byte) (encodedData string, err error) { 13 | pemBlock, _ := pem.Decode(b) 14 | if pemBlock == nil { 15 | return "", fmt.Errorf("%w", errPEMDecode) 16 | } 17 | 18 | der := pemBlock.Bytes 19 | encodedData = base64.StdEncoding.EncodeToString(der) 20 | return encodedData, nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/openvpn/extract/read.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func readCustomConfigLines(filepath string) ( 10 | lines []string, err error, 11 | ) { 12 | file, err := os.Open(filepath) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | b, err := io.ReadAll(file) 18 | if err != nil { 19 | _ = file.Close() 20 | return nil, err 21 | } 22 | 23 | if err := file.Close(); err != nil { 24 | return nil, err 25 | } 26 | 27 | return strings.Split(string(b), "\n"), nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/openvpn/extract/read_test.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_readCustomConfigLines(t *testing.T) { 12 | t.Parallel() 13 | 14 | file, err := os.CreateTemp("", "") 15 | require.NoError(t, err) 16 | defer removeFile(t, file.Name()) 17 | defer file.Close() 18 | 19 | _, err = file.WriteString("line one\nline two\nline three\n") 20 | require.NoError(t, err) 21 | 22 | err = file.Close() 23 | require.NoError(t, err) 24 | 25 | lines, err := readCustomConfigLines(file.Name()) 26 | assert.NoError(t, err) 27 | 28 | expectedLines := []string{ 29 | "line one", "line two", "line three", "", 30 | } 31 | assert.Equal(t, expectedLines, lines) 32 | } 33 | -------------------------------------------------------------------------------- /internal/openvpn/interfaces.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | import "os/exec" 4 | 5 | type CmdStarter interface { 6 | Start(cmd *exec.Cmd) ( 7 | stdoutLines, stderrLines <-chan string, 8 | waitError <-chan error, startErr error) 9 | } 10 | 11 | type CmdRunStarter interface { 12 | Run(cmd *exec.Cmd) (output string, err error) 13 | CmdStarter 14 | } 15 | -------------------------------------------------------------------------------- /internal/openvpn/logger.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | type Logger interface { 4 | Debug(s string) 5 | Infoer 6 | Warn(s string) 7 | Error(s string) 8 | } 9 | 10 | type Infoer interface { 11 | Info(s string) 12 | } 13 | -------------------------------------------------------------------------------- /internal/openvpn/openvpn.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/constants/openvpn" 5 | ) 6 | 7 | type Configurator struct { 8 | logger Infoer 9 | cmder CmdRunStarter 10 | configPath string 11 | authFilePath string 12 | askPassPath string 13 | puid, pgid int 14 | } 15 | 16 | func New(logger Infoer, cmder CmdRunStarter, 17 | puid, pgid int, 18 | ) *Configurator { 19 | return &Configurator{ 20 | logger: logger, 21 | cmder: cmder, 22 | configPath: configPath, 23 | authFilePath: openvpn.AuthConf, 24 | askPassPath: openvpn.AskPassPath, 25 | puid: puid, 26 | pgid: pgid, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/openvpn/paths.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | const configPath = "/etc/openvpn/target.ovpn" 4 | -------------------------------------------------------------------------------- /internal/openvpn/pkcs8/testdata/readme.txt: -------------------------------------------------------------------------------- 1 | The key files in this directory are generated using OpenSSL. 2 | Re-generating them is fine and should work with existing tests. 3 | 4 | For DES encrypted RSA key files, openssl version 1.x.x is required, and the following commands in order generate the files: 5 | 6 | openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:512 -des -pass pass:password -out rsa_pkcs8_aes128cbc_encrypted.pem 7 | openssl pkcs8 -topk8 -in rsa_pkcs8_aes128cbc_encrypted.pem -passin pass:password -nocrypt -out rsa_pkcs8_aes128cbc_decrypted.pem 8 | 9 | For AES encrypted RSA key files, the following commands in order generate the files: 10 | 11 | openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:512 -aes-128-cbc -pass pass:password -out rsa_pkcs8_descbc_encrypted.pem 12 | openssl pkcs8 -topk8 -in rsa_pkcs8_descbc_encrypted.pem -passin pass:password -nocrypt -out rsa_pkcs8_descbc_decrypted.pem 13 | -------------------------------------------------------------------------------- /internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_decrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAsont6TMS9RVqjXoi 3 | wF/oKZCwbWM4HmCJvp5Z2dOfKabt+7FOTJiD7APLJKva6791HDTuyBu7+HFQCzW3 4 | ghLuiwIDAQABAkB09FuwHq/1cmEJao+nO2xHBiw8i/lwFMdG4k5znegujL4g16i7 5 | +afWrMd54jYNPGiKuSNObB2BZR1j8tz/jvbxAiEA3d7bVwtWdaZVIV5t9uqrq5fG 6 | j3eXfNemTu1HQDmVqNMCIQDOALECY98KURR4NJueTKNuvawkuWFhizfKKTfS5B6Q 7 | aQIhANsF/RFYp+lMYg2m4nc2AnJKSkGmlW0wlYSkyAmmzw7xAiEAqSz+MSVNnU5a 8 | ziD+D/GGYkKYJYysgYvwZDCXbLT0uMkCIQDZghteTq2MMwIWWUJti3nc6nCICaJu 9 | d5O9Sm7BcOSuoA== 10 | -----END PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /internal/openvpn/pkcs8/testdata/rsa_pkcs8_aes128cbc_encrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIX7rAZ9pfZ4ACAggA 3 | MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBBVQ5G606jCKrKADBAiKwcPBIIB 4 | YBbudvVfqdLKm9LBFOAcUQk+sdFrq6e2r/xnuqM7VY6Ru4pMOmVMhHMMCFkqHLjx 5 | f7hN+xjk3XpYyoptnozPBOhypZrjd6IeEJSkBtU5BZR8fP0Bhny5NYHGcyPR6MZA 6 | 5iX/0fnyMlrncG67UNwoZQjfg7jEO3mAjuCW/F74xtPQ90ZHtw8mYC26fa09uQR4 7 | ptL9XqZuw4+U//3CuOheKqI17wulKAb4NwJckYbKyOik+J4yAi0ScgO73pD1FFvl 8 | qBxcpyvEqFQqkOlcbR9YwVBAXeW8cbpZJd+MilSs7Ru/phHrP9wz5chYDrocbeG/ 9 | H8FhCCvZnJ3zC3P3FPRNPtoaduJ0MbYpaMv4hyP3tEbzbslPA1v14ES3U+w0gmdD 10 | zpsy0oplQK9d9wL2TKBwyALcUx5BhtcqKsUXwBOWXMToc4lIXUVl0UVYwULibmEd 11 | yK6ajugNxG95X+BJjGvWu/U= 12 | -----END ENCRYPTED PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_decrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAuU3FTtbPm8OjZ/d8 3 | vVd+seQcrCGgwxigKpOszFfOOXKxfy2CgpjE1Ga2h0UneJ6pq0KZyY+ggYAX8PaS 4 | U6R3HwIDAQABAkEAibQPkjzz3u8Nua8i1Zn1nsDDxe7fhtv/+mPvn5MIv4sFRS71 5 | 0o9+SPNIQn7aJcGIqyBzHYdQg3/wGla+LA+msQIhAOt+hy1dnaWTSXIrIuPt+sSP 6 | Fjk80ijfxntXHNU6qExjAiEAyXBurrTdQs6D61ZzdlOFzgUs/FHa4dmWmxXuFsdv 7 | 8RUCIQCIZQJaLiyOp94UOBO/PCjQC6ftguKeNe25plzWy2CKzQIgXBpBMTZXGG2u 8 | WZMcldSYkFtDd1bB2pQPTXeYdefYYgUCIQDVH3ysySFXIlHJulgcxvriXTfY4goY 9 | TQ0PL0Ow7sIz6A== 10 | -----END PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /internal/openvpn/pkcs8/testdata/rsa_pkcs8_descbc_encrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIBsTBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQIZK8yPqcvVqoCAggA 3 | MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECI7C8b+gk6UJBIIBYGQQ4UcglyUqSFC7 4 | JiA+Gh01K1odfdLJKLh30+iescrFenII4Vv4rX5609URhn2iHCXhlnZ0+9geRR9k 5 | dQSKXaDVVGQw3bQUKgS+lZDAeLV4PS7c+KW0xLpXWJxBPs6NXQMxoJZ23UA391EH 6 | p8gKzZqUKk/rEOP68wr3IpHqaD3xggzN+4eA4ZKj4OktmWfUjgC7RQIZSaMxfq+D 7 | q+4D5onp+B4C2WRfjnN/N2g7UhzKWGvhjKyogvl82PuY9Vp1qPwQGdg5wdJ/2UVX 8 | QNvbkT21Wrv1ffFuIDS1/lCPnd8RAl2Q7chfLyut4BjP0tlmYNxRwQU2mT3KZOrB 9 | wwhWgXZtBwj4LjyasVkKe4hyVfRXN5NgONvqxof3VdZUHzOegOapNbEmfhNwVogj 10 | 1gwRWL7etAbYKjiMPFzZJAiU97+UkqveguldeoHmvWRDTLqxgZw5M4wkPPldb+u8 11 | d1vCDDQ= 12 | -----END ENCRYPTED PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /internal/openvpn/start.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os/exec" 8 | "syscall" 9 | 10 | "github.com/qdm12/gluetun/internal/constants/openvpn" 11 | ) 12 | 13 | var ErrVersionUnknown = errors.New("OpenVPN version is unknown") 14 | 15 | const ( 16 | binOpenvpn25 = "openvpn2.5" 17 | binOpenvpn26 = "openvpn2.6" 18 | ) 19 | 20 | func start(ctx context.Context, starter CmdStarter, version string, flags []string) ( 21 | stdoutLines, stderrLines <-chan string, waitError <-chan error, err error, 22 | ) { 23 | var bin string 24 | switch version { 25 | case openvpn.Openvpn25: 26 | bin = binOpenvpn25 27 | case openvpn.Openvpn26: 28 | bin = binOpenvpn26 29 | default: 30 | return nil, nil, nil, fmt.Errorf("%w: %s", ErrVersionUnknown, version) 31 | } 32 | 33 | args := []string{"--config", configPath} 34 | args = append(args, flags...) 35 | cmd := exec.CommandContext(ctx, bin, args...) 36 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 37 | 38 | return starter.Start(cmd) 39 | } 40 | -------------------------------------------------------------------------------- /internal/openvpn/stream.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | ) 7 | 8 | func streamLines(ctx context.Context, done chan<- struct{}, 9 | logger Logger, stdout, stderr <-chan string, 10 | tunnelReady chan<- struct{}, 11 | ) { 12 | defer close(done) 13 | 14 | var line string 15 | 16 | for { 17 | errLine := false 18 | select { 19 | case <-ctx.Done(): 20 | return 21 | case line = <-stdout: 22 | case line = <-stderr: 23 | errLine = true 24 | } 25 | line, level := processLogLine(line) 26 | if line == "" { 27 | continue // filtered out 28 | } 29 | if errLine { 30 | level = levelError 31 | } 32 | switch level { 33 | case levelInfo: 34 | logger.Info(line) 35 | case levelWarn: 36 | logger.Warn(line) 37 | case levelError: 38 | logger.Error(line) 39 | } 40 | if strings.Contains(line, "Initialization Sequence Completed") { 41 | // do not close tunnelReady in case the initialization 42 | // happens multiple times without Openvpn restarting 43 | tunnelReady <- struct{}{} 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/openvpn/version.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | func (c *Configurator) Version25(ctx context.Context) (version string, err error) { 12 | return c.version(ctx, binOpenvpn25) 13 | } 14 | 15 | func (c *Configurator) Version26(ctx context.Context) (version string, err error) { 16 | return c.version(ctx, binOpenvpn26) 17 | } 18 | 19 | var ErrVersionTooShort = errors.New("version output is too short") 20 | 21 | func (c *Configurator) version(ctx context.Context, binName string) (version string, err error) { 22 | cmd := exec.CommandContext(ctx, binName, "--version") 23 | output, err := c.cmder.Run(cmd) 24 | if err != nil && err.Error() != "exit status 1" { 25 | return "", err 26 | } 27 | firstLine := strings.Split(output, "\n")[0] 28 | words := strings.Fields(firstLine) 29 | const minWords = 2 30 | if len(words) < minWords { 31 | return "", fmt.Errorf("%w: %s", ErrVersionTooShort, firstLine) 32 | } 33 | return words[1], nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/portforward/interfaces.go: -------------------------------------------------------------------------------- 1 | package portforward 2 | 3 | import ( 4 | "context" 5 | "net/netip" 6 | "os/exec" 7 | ) 8 | 9 | type Service interface { 10 | Start(ctx context.Context) (runError <-chan error, err error) 11 | Stop() (err error) 12 | GetPortsForwarded() (ports []uint16) 13 | } 14 | 15 | type Routing interface { 16 | VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error) 17 | AssignedIP(interfaceName string, family int) (ip netip.Addr, err error) 18 | } 19 | 20 | type PortAllower interface { 21 | SetAllowedPort(ctx context.Context, port uint16, intf string) (err error) 22 | RemoveAllowedPort(ctx context.Context, port uint16) (err error) 23 | RedirectPort(ctx context.Context, intf string, sourcePort, 24 | destinationPort uint16) (err error) 25 | } 26 | 27 | type Logger interface { 28 | Debug(s string) 29 | Info(s string) 30 | Warn(s string) 31 | Error(s string) 32 | } 33 | 34 | type Cmder interface { 35 | Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string, 36 | waitError <-chan error, startErr error) 37 | } 38 | -------------------------------------------------------------------------------- /internal/portforward/service/command_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package service 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | gomock "github.com/golang/mock/gomock" 10 | "github.com/qdm12/gluetun/internal/command" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func Test_Service_runCommand(t *testing.T) { 15 | t.Parallel() 16 | ctrl := gomock.NewController(t) 17 | 18 | ctx := context.Background() 19 | cmder := command.New() 20 | const commandTemplate = `/bin/sh -c "echo {{PORTS}}"` 21 | ports := []uint16{1234, 5678} 22 | logger := NewMockLogger(ctrl) 23 | logger.EXPECT().Info("1234,5678") 24 | 25 | err := runCommand(ctx, cmder, logger, commandTemplate, ports) 26 | 27 | require.NoError(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /internal/portforward/service/fs.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func (s *Service) writePortForwardedFile(ports []uint16) (err error) { 10 | portStrings := make([]string, len(ports)) 11 | for i, port := range ports { 12 | portStrings[i] = fmt.Sprint(int(port)) 13 | } 14 | fileData := []byte(strings.Join(portStrings, "\n")) 15 | 16 | filepath := s.settings.Filepath 17 | s.logger.Info("writing port file " + filepath) 18 | const perms = os.FileMode(0o644) 19 | err = os.WriteFile(filepath, fileData, perms) 20 | if err != nil { 21 | return fmt.Errorf("writing file: %w", err) 22 | } 23 | 24 | err = os.Chown(filepath, s.puid, s.pgid) 25 | if err != nil { 26 | return fmt.Errorf("chowning file: %w", err) 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/portforward/service/helpers.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func portsToString(ports []uint16) (s string) { 9 | switch len(ports) { 10 | case 0: 11 | return "no port forwarded" 12 | case 1: 13 | return "port forwarded is " + fmt.Sprint(int(ports[0])) 14 | default: 15 | portStrings := make([]string, len(ports)) 16 | for i, port := range ports { 17 | portStrings[i] = fmt.Sprint(int(port)) 18 | } 19 | return "ports forwarded are " + strings.Join(portStrings[:len(portStrings)-1], ", ") + 20 | " and " + portStrings[len(portStrings)-1] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/portforward/service/helpers_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_portsToString(t *testing.T) { 10 | t.Parallel() 11 | 12 | testCases := map[string]struct { 13 | ports []uint16 14 | s string 15 | }{ 16 | "no_port": { 17 | s: "no port forwarded", 18 | }, 19 | "one_port": { 20 | ports: []uint16{123}, 21 | s: "port forwarded is 123", 22 | }, 23 | "two_ports": { 24 | ports: []uint16{123, 456}, 25 | s: "ports forwarded are 123 and 456", 26 | }, 27 | "three_ports": { 28 | ports: []uint16{123, 456, 789}, 29 | s: "ports forwarded are 123, 456 and 789", 30 | }, 31 | } 32 | 33 | for name, testCase := range testCases { 34 | t.Run(name, func(t *testing.T) { 35 | t.Parallel() 36 | 37 | s := portsToString(testCase.ports) 38 | 39 | assert.Equal(t, testCase.s, s) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/portforward/service/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | //go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger 4 | -------------------------------------------------------------------------------- /internal/pprof/helpers_test.go: -------------------------------------------------------------------------------- 1 | package pprof 2 | 3 | import ( 4 | "regexp" 5 | 6 | gomock "github.com/golang/mock/gomock" 7 | ) 8 | 9 | func boolPtr(b bool) *bool { return &b } 10 | 11 | func intPtr(n int) *int { return &n } 12 | 13 | var _ gomock.Matcher = (*regexMatcher)(nil) 14 | 15 | type regexMatcher struct { 16 | regexp *regexp.Regexp 17 | } 18 | 19 | func (r *regexMatcher) Matches(x interface{}) bool { 20 | s, ok := x.(string) 21 | if !ok { 22 | return false 23 | } 24 | return r.regexp.MatchString(s) 25 | } 26 | 27 | func (r *regexMatcher) String() string { 28 | return "regular expression " + r.regexp.String() 29 | } 30 | 31 | func newRegexMatcher(regex string) *regexMatcher { 32 | return ®exMatcher{ 33 | regexp: regexp.MustCompile(regex), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/provider/airvpn/connection.go: -------------------------------------------------------------------------------- 1 | package airvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 1194, 1637) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/airvpn/provider.go: -------------------------------------------------------------------------------- 1 | package airvpn 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/airvpn/updater" 9 | "github.com/qdm12/gluetun/internal/provider/common" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(client), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Airvpn 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/airvpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Updater struct { 8 | client *http.Client 9 | } 10 | 11 | func New(client *http.Client) *Updater { 12 | return &Updater{ 13 | client: client, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/common/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // Exceptionally, these mocks are exported since they are used by all 4 | // provider subpackages tests, and it reduces test code duplication a lot. 5 | // Note mocks.go might need to be removed before re-generating it. 6 | //go:generate mockgen -destination=mocks.go -package $GOPACKAGE . ParallelResolver,Storage,Unzipper,Warner 7 | -------------------------------------------------------------------------------- /internal/provider/common/portforward.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | var ErrPortForwardNotSupported = errors.New("port forwarding not supported") 6 | -------------------------------------------------------------------------------- /internal/provider/common/storage.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | ) 7 | 8 | type Storage interface { 9 | FilterServers(provider string, selection settings.ServerSelection) ( 10 | servers []models.Server, err error) 11 | } 12 | -------------------------------------------------------------------------------- /internal/provider/custom/interfaces.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import "github.com/qdm12/gluetun/internal/models" 4 | 5 | type Extractor interface { 6 | Data(filepath string) (lines []string, 7 | connection models.Connection, err error) 8 | } 9 | -------------------------------------------------------------------------------- /internal/provider/custom/provider.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/constants/providers" 5 | "github.com/qdm12/gluetun/internal/provider/common" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | type Provider struct { 10 | extractor Extractor 11 | common.Fetcher 12 | } 13 | 14 | func New(extractor Extractor) *Provider { 15 | return &Provider{ 16 | extractor: extractor, 17 | Fetcher: utils.NewNoFetcher(providers.Custom), 18 | } 19 | } 20 | 21 | func (p *Provider) Name() string { 22 | return providers.Custom 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/cyberghost/connection.go: -------------------------------------------------------------------------------- 1 | package cyberghost 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 443, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/cyberghost/provider.go: -------------------------------------------------------------------------------- 1 | package cyberghost 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/cyberghost/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | parallelResolver common.ParallelResolver, 19 | ) *Provider { 20 | return &Provider{ 21 | storage: storage, 22 | randSource: randSource, 23 | Fetcher: updater.New(parallelResolver), 24 | } 25 | } 26 | 27 | func (p *Provider) Name() string { 28 | return providers.Cyberghost 29 | } 30 | -------------------------------------------------------------------------------- /internal/provider/cyberghost/updater/countries.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | func mergeCountryCodes(base, extend map[string]string) (merged map[string]string) { 4 | merged = make(map[string]string, len(base)) 5 | for countryCode, region := range base { 6 | merged[countryCode] = region 7 | } 8 | for countryCode := range base { 9 | delete(extend, countryCode) 10 | } 11 | for countryCode, region := range extend { 12 | merged[countryCode] = region 13 | } 14 | return merged 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/cyberghost/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 4 15 | maxFails = 10 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/cyberghost/updater/servers.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/qdm12/gluetun/internal/models" 9 | "github.com/qdm12/gluetun/internal/provider/common" 10 | ) 11 | 12 | func (u *Updater) FetchServers(ctx context.Context, minServers int) ( 13 | servers []models.Server, err error, 14 | ) { 15 | possibleServers := getPossibleServers() 16 | 17 | possibleHosts := possibleServers.hostsSlice() 18 | resolveSettings := parallelResolverSettings(possibleHosts) 19 | hostToIPs, _, err := u.parallelResolver.Resolve(ctx, resolveSettings) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | if len(hostToIPs) < minServers { 25 | return nil, fmt.Errorf("%w: %d and expected at least %d", 26 | common.ErrNotEnoughServers, len(servers), minServers) 27 | } 28 | 29 | possibleServers.adaptWithIPs(hostToIPs) 30 | 31 | servers = possibleServers.toSlice() 32 | 33 | sort.Sort(models.SortableServers(servers)) 34 | 35 | return servers, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/provider/cyberghost/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | parallelResolver common.ParallelResolver 9 | } 10 | 11 | func New(parallelResolver common.ParallelResolver) *Updater { 12 | return &Updater{ 13 | parallelResolver: parallelResolver, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/example/connection.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | // TODO: Set the default ports for each VPN protocol+network protocol 13 | // combination. If one combination is not supported, set it to `0`. 14 | defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:mnd 15 | return utils.GetConnection(p.Name(), 16 | p.storage, selection, defaults, ipv6Supported, p.randSource) 17 | } 18 | -------------------------------------------------------------------------------- /internal/provider/example/provider.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/example/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | // TODO: remove unneeded arguments once the updater is implemented. 19 | func New(storage common.Storage, randSource rand.Source, 20 | updaterWarner common.Warner, client *http.Client, 21 | unzipper common.Unzipper, parallelResolver common.ParallelResolver, 22 | ) *Provider { 23 | return &Provider{ 24 | storage: storage, 25 | randSource: randSource, 26 | Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver), 27 | } 28 | } 29 | 30 | func (p *Provider) Name() string { 31 | // TODO: update the constant to be the right provider name. 32 | return providers.Example 33 | } 34 | -------------------------------------------------------------------------------- /internal/provider/example/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | // TODO: remove this file if the parallel resolver is not used 10 | // by the updater. 11 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 12 | // TODO: adapt these constant values below to make the resolution 13 | // as fast and as reliable as possible. 14 | const ( 15 | maxFailRatio = 0.1 16 | maxDuration = 20 * time.Second 17 | betweenDuration = time.Second 18 | maxNoNew = 2 19 | maxFails = 2 20 | ) 21 | return resolver.ParallelSettings{ 22 | Hosts: hosts, 23 | MaxFailRatio: maxFailRatio, 24 | Repeat: resolver.RepeatSettings{ 25 | MaxDuration: maxDuration, 26 | BetweenDuration: betweenDuration, 27 | MaxNoNew: maxNoNew, 28 | MaxFails: maxFails, 29 | SortIPs: true, 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/provider/example/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | // TODO: remove fields not used by the updater 11 | client *http.Client 12 | unzipper common.Unzipper 13 | parallelResolver common.ParallelResolver 14 | warner common.Warner 15 | } 16 | 17 | func New(warner common.Warner, unzipper common.Unzipper, 18 | client *http.Client, parallelResolver common.ParallelResolver, 19 | ) *Updater { 20 | // TODO: remove arguments not used by the updater 21 | return &Updater{ 22 | client: client, 23 | unzipper: unzipper, 24 | parallelResolver: parallelResolver, 25 | warner: warner, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/provider/expressvpn/connection.go: -------------------------------------------------------------------------------- 1 | package expressvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(0, 1195, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/expressvpn/provider.go: -------------------------------------------------------------------------------- 1 | package expressvpn 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/expressvpn/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Expressvpn 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/expressvpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxNoNew = 1 13 | maxFails = 2 14 | ) 15 | return resolver.ParallelSettings{ 16 | Hosts: hosts, 17 | MaxFailRatio: maxFailRatio, 18 | Repeat: resolver.RepeatSettings{ 19 | MaxDuration: time.Second, 20 | MaxNoNew: maxNoNew, 21 | MaxFails: maxFails, 22 | SortIPs: true, 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/provider/expressvpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | parallelResolver common.ParallelResolver 10 | warner common.Warner 11 | } 12 | 13 | func New(unzipper common.Unzipper, warner common.Warner, 14 | parallelResolver common.ParallelResolver, 15 | ) *Updater { 16 | return &Updater{ 17 | unzipper: unzipper, 18 | parallelResolver: parallelResolver, 19 | warner: warner, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/fastestvpn/connection.go: -------------------------------------------------------------------------------- 1 | package fastestvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(4443, 4443, 51820) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/fastestvpn/provider.go: -------------------------------------------------------------------------------- 1 | package fastestvpn 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/fastestvpn/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, updaterWarner common.Warner, 20 | parallelResolver common.ParallelResolver, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(client, updaterWarner, parallelResolver), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.Fastestvpn 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/fastestvpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxNoNew = 1 13 | maxFails = 4 14 | maxDuration = 3 * time.Second 15 | ) 16 | return resolver.ParallelSettings{ 17 | Hosts: hosts, 18 | MaxFailRatio: maxFailRatio, 19 | Repeat: resolver.RepeatSettings{ 20 | MaxDuration: maxDuration, 21 | MaxNoNew: maxNoNew, 22 | MaxFails: maxFails, 23 | SortIPs: true, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/provider/fastestvpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | parallelResolver common.ParallelResolver 12 | warner common.Warner 13 | } 14 | 15 | func New(client *http.Client, warner common.Warner, 16 | parallelResolver common.ParallelResolver, 17 | ) *Updater { 18 | return &Updater{ 19 | client: client, 20 | parallelResolver: parallelResolver, 21 | warner: warner, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/giganews/connection.go: -------------------------------------------------------------------------------- 1 | package giganews 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/giganews/provider.go: -------------------------------------------------------------------------------- 1 | package giganews 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/giganews/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Giganews 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/giganews/updater/filename.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var errNotOvpnExt = errors.New("filename does not have the openvpn file extension") 10 | 11 | func parseFilename(fileName string) ( 12 | region string, err error, 13 | ) { 14 | const suffix = ".ovpn" 15 | if !strings.HasSuffix(fileName, suffix) { 16 | return "", fmt.Errorf("%w: %s", errNotOvpnExt, fileName) 17 | } 18 | 19 | region = strings.TrimSuffix(fileName, suffix) 20 | region = strings.ReplaceAll(region, " - ", " ") 21 | return region, nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/provider/giganews/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 5 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/giganews/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | parallelResolver common.ParallelResolver 10 | warner common.Warner 11 | } 12 | 13 | func New(unzipper common.Unzipper, warner common.Warner, 14 | parallelResolver common.ParallelResolver, 15 | ) *Updater { 16 | return &Updater{ 17 | unzipper: unzipper, 18 | parallelResolver: parallelResolver, 19 | warner: warner, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/hidemyass/connection.go: -------------------------------------------------------------------------------- 1 | package hidemyass 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(8080, 553, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/hidemyass/provider.go: -------------------------------------------------------------------------------- 1 | package hidemyass 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/hidemyass/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, updaterWarner common.Warner, 20 | parallelResolver common.ParallelResolver, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(client, updaterWarner, parallelResolver), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.HideMyAss 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/hidemyass/updater/hosts.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | func getUniqueHosts(tcpHostToURL, udpHostToURL map[string]string) ( 4 | hosts []string, 5 | ) { 6 | uniqueHosts := make(map[string]struct{}, len(tcpHostToURL)) 7 | for host := range tcpHostToURL { 8 | uniqueHosts[host] = struct{}{} 9 | } 10 | for host := range udpHostToURL { 11 | uniqueHosts[host] = struct{}{} 12 | } 13 | 14 | hosts = make([]string, 0, len(uniqueHosts)) 15 | for host := range uniqueHosts { 16 | hosts = append(hosts, host) 17 | } 18 | 19 | return hosts 20 | } 21 | -------------------------------------------------------------------------------- /internal/provider/hidemyass/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 15 * time.Second 13 | betweenDuration = 2 * time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/hidemyass/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | parallelResolver common.ParallelResolver 12 | warner common.Warner 13 | } 14 | 15 | func New(client *http.Client, warner common.Warner, 16 | parallelResolver common.ParallelResolver, 17 | ) *Updater { 18 | return &Updater{ 19 | client: client, 20 | parallelResolver: parallelResolver, 21 | warner: warner, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/ipvanish/connection.go: -------------------------------------------------------------------------------- 1 | package ipvanish 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/ipvanish/provider.go: -------------------------------------------------------------------------------- 1 | package ipvanish 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/ipvanish/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Ipvanish 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/ipvanish/updater/filename.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/qdm12/gluetun/internal/constants" 9 | "golang.org/x/text/cases" 10 | ) 11 | 12 | var errCountryCodeUnknown = errors.New("country code is unknown") 13 | 14 | func parseFilename(fileName, hostname string, titleCaser cases.Caser) ( 15 | country, city string, err error, 16 | ) { 17 | const prefix = "ipvanish-" 18 | s := strings.TrimPrefix(fileName, prefix) 19 | 20 | const ext = ".ovpn" 21 | host := strings.Split(hostname, ".")[0] 22 | suffix := "-" + host + ext 23 | s = strings.TrimSuffix(s, suffix) 24 | 25 | parts := strings.Split(s, "-") 26 | 27 | countryCodes := constants.CountryCodes() 28 | countryCode := strings.ToLower(parts[0]) 29 | country, ok := countryCodes[countryCode] 30 | if !ok { 31 | return "", "", fmt.Errorf("%w: %s", errCountryCodeUnknown, countryCode) 32 | } 33 | country = titleCaser.String(country) 34 | 35 | if len(parts) > 1 { 36 | city = strings.Join(parts[1:], " ") 37 | city = titleCaser.String(city) 38 | } 39 | 40 | return country, city, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/provider/ipvanish/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/ipvanish/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | warner common.Warner 10 | parallelResolver common.ParallelResolver 11 | } 12 | 13 | func New(unzipper common.Unzipper, warner common.Warner, 14 | parallelResolver common.ParallelResolver, 15 | ) *Updater { 16 | return &Updater{ 17 | unzipper: unzipper, 18 | warner: warner, 19 | parallelResolver: parallelResolver, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/ivpn/connection.go: -------------------------------------------------------------------------------- 1 | package ivpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 1194, 58237) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/ivpn/provider.go: -------------------------------------------------------------------------------- 1 | package ivpn 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/ivpn/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, updaterWarner common.Warner, 20 | parallelResolver common.ParallelResolver, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(client, updaterWarner, parallelResolver), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.Ivpn 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/ivpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/ivpn/updater/roundtrip_test.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import "net/http" 4 | 5 | type roundTripFunc func(r *http.Request) (*http.Response, error) 6 | 7 | func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { 8 | return f(r) 9 | } 10 | -------------------------------------------------------------------------------- /internal/provider/ivpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | parallelResolver common.ParallelResolver 12 | warner common.Warner 13 | } 14 | 15 | func New(client *http.Client, warner common.Warner, 16 | parallelResolver common.ParallelResolver, 17 | ) *Updater { 18 | return &Updater{ 19 | client: client, 20 | parallelResolver: parallelResolver, 21 | warner: warner, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/mullvad/connection.go: -------------------------------------------------------------------------------- 1 | package mullvad 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/mullvad/provider.go: -------------------------------------------------------------------------------- 1 | package mullvad 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/mullvad/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(client), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Mullvad 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/mullvad/updater/ips.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/netip" 5 | "sort" 6 | ) 7 | 8 | func uniqueSortedIPs(ips []netip.Addr) []netip.Addr { 9 | uniqueIPs := make(map[string]struct{}, len(ips)) 10 | for _, ip := range ips { 11 | key := ip.String() 12 | uniqueIPs[key] = struct{}{} 13 | } 14 | 15 | ips = make([]netip.Addr, 0, len(uniqueIPs)) 16 | for key := range uniqueIPs { 17 | ip, err := netip.ParseAddr(key) 18 | if err != nil { 19 | panic(err) 20 | } 21 | if ip.Is4In6() { 22 | ip = netip.AddrFrom4(ip.As4()) 23 | } 24 | ips = append(ips, ip) 25 | } 26 | 27 | sort.Slice(ips, func(i, j int) bool { 28 | return ips[i].Compare(ips[j]) < 0 29 | }) 30 | 31 | return ips 32 | } 33 | -------------------------------------------------------------------------------- /internal/provider/mullvad/updater/servers.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/qdm12/gluetun/internal/models" 9 | "github.com/qdm12/gluetun/internal/provider/common" 10 | ) 11 | 12 | func (u *Updater) FetchServers(ctx context.Context, minServers int) ( 13 | servers []models.Server, err error, 14 | ) { 15 | data, err := fetchAPI(ctx, u.client) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | hts := make(hostToServer) 21 | for _, serverData := range data { 22 | if err := hts.add(serverData); err != nil { 23 | return nil, err 24 | } 25 | } 26 | 27 | if len(hts) < minServers { 28 | return nil, fmt.Errorf("%w: %d and expected at least %d", 29 | common.ErrNotEnoughServers, len(hts), minServers) 30 | } 31 | 32 | servers = hts.toServersSlice() 33 | 34 | sort.Sort(models.SortableServers(servers)) 35 | 36 | return servers, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/provider/mullvad/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Updater struct { 8 | client *http.Client 9 | } 10 | 11 | func New(client *http.Client) *Updater { 12 | return &Updater{ 13 | client: client, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/nordvpn/connection.go: -------------------------------------------------------------------------------- 1 | package nordvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/nordvpn/provider.go: -------------------------------------------------------------------------------- 1 | package nordvpn 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/nordvpn/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, updaterWarner common.Warner, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(client, updaterWarner), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Nordvpn 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/nordvpn/updater/name.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | ErrNoIDInServerName = errors.New("no ID in server name") 12 | ErrInvalidIDInServerName = errors.New("invalid ID in server name") 13 | ) 14 | 15 | func parseServerName(serverName string) (number uint16, err error) { 16 | i := strings.IndexRune(serverName, '#') 17 | if i < 0 { 18 | return 0, fmt.Errorf("%w: %s", ErrNoIDInServerName, serverName) 19 | } 20 | 21 | idString := serverName[i+1:] 22 | idUint64, err := strconv.ParseUint(idString, 10, 16) 23 | if err != nil { 24 | return 0, fmt.Errorf("%w: %s", ErrInvalidIDInServerName, serverName) 25 | } 26 | 27 | number = uint16(idUint64) 28 | return number, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/provider/nordvpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | warner common.Warner 12 | } 13 | 14 | func New(client *http.Client, warner common.Warner) *Updater { 15 | return &Updater{ 16 | client: client, 17 | warner: warner, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/provider/perfectprivacy/connection.go: -------------------------------------------------------------------------------- 1 | package perfectprivacy 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 443, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/perfectprivacy/portforward_test.go: -------------------------------------------------------------------------------- 1 | package perfectprivacy 2 | 3 | import ( 4 | "net/netip" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_internalIPToPorts(t *testing.T) { 11 | t.Parallel() 12 | 13 | testCases := map[string]struct { 14 | internalIP netip.Addr 15 | ports []uint16 16 | }{ 17 | "example_case": { 18 | internalIP: netip.AddrFrom4([4]byte{10, 0, 203, 88}), 19 | ports: []uint16{12904, 22904, 32904}, 20 | }, 21 | } 22 | 23 | for name, testCase := range testCases { 24 | t.Run(name, func(t *testing.T) { 25 | t.Parallel() 26 | 27 | ports := internalIPToPorts(testCase.internalIP) 28 | 29 | assert.Equal(t, testCase.ports, ports) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/provider/perfectprivacy/provider.go: -------------------------------------------------------------------------------- 1 | package perfectprivacy 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/perfectprivacy/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | ) *Provider { 20 | return &Provider{ 21 | storage: storage, 22 | randSource: randSource, 23 | Fetcher: updater.New(unzipper, updaterWarner), 24 | } 25 | } 26 | 27 | func (p *Provider) Name() string { 28 | return providers.Perfectprivacy 29 | } 30 | -------------------------------------------------------------------------------- /internal/provider/perfectprivacy/updater/filename.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | func parseFilename(fileName string) (city string) { 9 | const suffix = ".conf" 10 | s := strings.TrimSuffix(fileName, suffix) 11 | 12 | for i, r := range s { 13 | if unicode.IsDigit(r) { 14 | s = s[:i] 15 | break 16 | } 17 | } 18 | 19 | return s 20 | } 21 | -------------------------------------------------------------------------------- /internal/provider/perfectprivacy/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | warner common.Warner 10 | } 11 | 12 | func New(unzipper common.Unzipper, warner common.Warner) *Updater { 13 | return &Updater{ 14 | unzipper: unzipper, 15 | warner: warner, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/provider/privado/connection.go: -------------------------------------------------------------------------------- 1 | package privado 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(0, 1194, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/privado/provider.go: -------------------------------------------------------------------------------- 1 | package privado 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/privado/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | ipFetcher common.IPFetcher, unzipper common.Unzipper, 19 | updaterWarner common.Warner, 20 | parallelResolver common.ParallelResolver, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.Privado 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/privado/updater/location.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "context" 5 | "net/netip" 6 | 7 | "github.com/qdm12/gluetun/internal/models" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/publicip/api" 10 | ) 11 | 12 | func setLocationInfo(ctx context.Context, fetcher common.IPFetcher, servers []models.Server) (err error) { 13 | // Get public IP address information 14 | ipsToGetInfo := make([]netip.Addr, 0, len(servers)) 15 | for _, server := range servers { 16 | ipsToGetInfo = append(ipsToGetInfo, server.IPs...) 17 | } 18 | ipsInfo, err := api.FetchMultiInfo(ctx, fetcher, ipsToGetInfo) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | for i := range servers { 24 | ipInfo := ipsInfo[i] 25 | servers[i].Country = ipInfo.Country 26 | servers[i].Region = ipInfo.Region 27 | servers[i].City = ipInfo.City 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/privado/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 30 * time.Second 13 | maxNoNew = 2 14 | maxFails = 2 15 | ) 16 | return resolver.ParallelSettings{ 17 | Hosts: hosts, 18 | MaxFailRatio: maxFailRatio, 19 | Repeat: resolver.RepeatSettings{ 20 | MaxDuration: maxDuration, 21 | MaxNoNew: maxNoNew, 22 | MaxFails: maxFails, 23 | SortIPs: true, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/provider/privado/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | ipFetcher common.IPFetcher 9 | unzipper common.Unzipper 10 | parallelResolver common.ParallelResolver 11 | warner common.Warner 12 | } 13 | 14 | func New(ipFetcher common.IPFetcher, unzipper common.Unzipper, 15 | warner common.Warner, parallelResolver common.ParallelResolver, 16 | ) *Updater { 17 | return &Updater{ 18 | ipFetcher: ipFetcher, 19 | unzipper: unzipper, 20 | parallelResolver: parallelResolver, 21 | warner: warner, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/privateinternetaccess/connection.go: -------------------------------------------------------------------------------- 1 | package privateinternetaccess 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets" 7 | "github.com/qdm12/gluetun/internal/provider/utils" 8 | ) 9 | 10 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 11 | connection models.Connection, err error, 12 | ) { 13 | // Set port defaults depending on encryption preset. 14 | var defaults utils.ConnectionDefaults 15 | switch *selection.OpenVPN.PIAEncPreset { 16 | case presets.None, presets.Normal: 17 | defaults.OpenVPNTCPPort = 502 18 | defaults.OpenVPNUDPPort = 1198 19 | case presets.Strong: 20 | defaults.OpenVPNTCPPort = 501 21 | defaults.OpenVPNUDPPort = 1197 22 | } 23 | 24 | return utils.GetConnection(p.Name(), 25 | p.storage, selection, defaults, ipv6Supported, p.randSource) 26 | } 27 | -------------------------------------------------------------------------------- /internal/provider/privateinternetaccess/presets/presets.go: -------------------------------------------------------------------------------- 1 | package presets 2 | 3 | const ( 4 | None = "none" 5 | Normal = "normal" 6 | Strong = "strong" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/provider/privateinternetaccess/provider.go: -------------------------------------------------------------------------------- 1 | package privateinternetaccess 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/qdm12/gluetun/internal/constants/providers" 9 | "github.com/qdm12/gluetun/internal/provider/common" 10 | "github.com/qdm12/gluetun/internal/provider/privateinternetaccess/updater" 11 | ) 12 | 13 | type Provider struct { 14 | storage common.Storage 15 | randSource rand.Source 16 | timeNow func() time.Time 17 | common.Fetcher 18 | // Port forwarding 19 | portForwardPath string 20 | } 21 | 22 | func New(storage common.Storage, randSource rand.Source, 23 | timeNow func() time.Time, client *http.Client, 24 | ) *Provider { 25 | const jsonPortForwardPath = "/gluetun/piaportforward.json" 26 | return &Provider{ 27 | storage: storage, 28 | timeNow: timeNow, 29 | randSource: randSource, 30 | portForwardPath: jsonPortForwardPath, 31 | Fetcher: updater.New(client), 32 | } 33 | } 34 | 35 | func (p *Provider) Name() string { 36 | return providers.PrivateInternetAccess 37 | } 38 | -------------------------------------------------------------------------------- /internal/provider/privateinternetaccess/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Updater struct { 8 | client *http.Client 9 | } 10 | 11 | func New(client *http.Client) *Updater { 12 | return &Updater{ 13 | client: client, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/privatevpn/connection.go: -------------------------------------------------------------------------------- 1 | package privatevpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 1194, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/privatevpn/provider.go: -------------------------------------------------------------------------------- 1 | package privatevpn 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/privatevpn/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Privatevpn 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/privatevpn/updater/countries.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import "strings" 4 | 5 | func codeToCountry(countryCode string, countryCodes map[string]string) ( 6 | country string, warning string, 7 | ) { 8 | countryCode = strings.ToLower(countryCode) 9 | country, ok := countryCodes[countryCode] 10 | if !ok { 11 | warning = "unknown country code: " + countryCode 12 | country = countryCode 13 | } 14 | return country, warning 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/privatevpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 6 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/privatevpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | parallelResolver common.ParallelResolver 10 | warner common.Warner 11 | } 12 | 13 | func New(unzipper common.Unzipper, warner common.Warner, 14 | parallelResolver common.ParallelResolver, 15 | ) *Updater { 16 | return &Updater{ 17 | unzipper: unzipper, 18 | parallelResolver: parallelResolver, 19 | warner: warner, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/protonvpn/connection.go: -------------------------------------------------------------------------------- 1 | package protonvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/protonvpn/provider.go: -------------------------------------------------------------------------------- 1 | package protonvpn 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/protonvpn/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | portForwarded uint16 17 | } 18 | 19 | func New(storage common.Storage, randSource rand.Source, 20 | client *http.Client, updaterWarner common.Warner, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(client, updaterWarner), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.Protonvpn 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/protonvpn/updater/countries.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import "strings" 4 | 5 | func codeToCountry(countryCode string, countryCodes map[string]string) ( 6 | country string, warning string, 7 | ) { 8 | countryCode = strings.ToLower(countryCode) 9 | country, ok := countryCodes[countryCode] 10 | if !ok { 11 | warning = "unknown country code: " + countryCode 12 | country = countryCode 13 | } 14 | return country, warning 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/protonvpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | warner common.Warner 12 | } 13 | 14 | func New(client *http.Client, warner common.Warner) *Updater { 15 | return &Updater{ 16 | client: client, 17 | warner: warner, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/configuration/settings" 7 | "github.com/qdm12/gluetun/internal/models" 8 | ) 9 | 10 | // Provider contains methods to read and modify the openvpn configuration to connect as a client. 11 | type Provider interface { 12 | GetConnection(selection settings.ServerSelection, ipv6Supported bool) (connection models.Connection, err error) 13 | OpenVPNConfig(connection models.Connection, settings settings.OpenVPN, ipv6Supported bool) (lines []string) 14 | Name() string 15 | FetchServers(ctx context.Context, minServers int) ( 16 | servers []models.Server, err error) 17 | } 18 | -------------------------------------------------------------------------------- /internal/provider/purevpn/connection.go: -------------------------------------------------------------------------------- 1 | package purevpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(80, 53, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/purevpn/provider.go: -------------------------------------------------------------------------------- 1 | package purevpn 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/purevpn/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | ipFetcher common.IPFetcher, unzipper common.Unzipper, 19 | updaterWarner common.Warner, parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Purevpn 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/purevpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/purevpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | ipFetcher common.IPFetcher 9 | unzipper common.Unzipper 10 | parallelResolver common.ParallelResolver 11 | warner common.Warner 12 | } 13 | 14 | func New(ipFetcher common.IPFetcher, unzipper common.Unzipper, 15 | warner common.Warner, parallelResolver common.ParallelResolver, 16 | ) *Updater { 17 | return &Updater{ 18 | ipFetcher: ipFetcher, 19 | unzipper: unzipper, 20 | parallelResolver: parallelResolver, 21 | warner: warner, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/slickvpn/connection.go: -------------------------------------------------------------------------------- 1 | package slickvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 443, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), p.storage, selection, defaults, ipv6Supported, p.randSource) 14 | } 15 | -------------------------------------------------------------------------------- /internal/provider/slickvpn/provider.go: -------------------------------------------------------------------------------- 1 | package slickvpn 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/slickvpn/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, updaterWarner common.Warner, 20 | parallelResolver common.ParallelResolver, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(client, updaterWarner, parallelResolver), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.SlickVPN 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/slickvpn/updater/helpers_test.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "golang.org/x/net/html" 10 | ) 11 | 12 | func parseTestHTML(t *testing.T, htmlString string) *html.Node { 13 | t.Helper() 14 | rootNode, err := html.Parse(strings.NewReader(htmlString)) 15 | require.NoError(t, err) 16 | return rootNode 17 | } 18 | 19 | func parseTestDataIndexHTML(t *testing.T) *html.Node { 20 | t.Helper() 21 | 22 | data, err := os.ReadFile("testdata/index.html") 23 | require.NoError(t, err) 24 | 25 | return parseTestHTML(t, string(data)) 26 | } 27 | -------------------------------------------------------------------------------- /internal/provider/slickvpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/slickvpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | parallelResolver common.ParallelResolver 12 | warner common.Warner 13 | } 14 | 15 | func New(client *http.Client, warner common.Warner, 16 | parallelResolver common.ParallelResolver, 17 | ) *Updater { 18 | return &Updater{ 19 | client: client, 20 | parallelResolver: parallelResolver, 21 | warner: warner, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/surfshark/connection.go: -------------------------------------------------------------------------------- 1 | package surfshark 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(1443, 1194, 51820) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/surfshark/provider.go: -------------------------------------------------------------------------------- 1 | package surfshark 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/surfshark/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, unzipper common.Unzipper, updaterWarner common.Warner, 20 | parallelResolver common.ParallelResolver, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(client, unzipper, updaterWarner, parallelResolver), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.Surfshark 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/surfshark/updater/location.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/qdm12/gluetun/internal/provider/surfshark/servers" 8 | ) 9 | 10 | var errHostnameNotFound = errors.New("hostname not found in hostname to location mapping") 11 | 12 | func getHostInformation(host string, hostnameToLocation map[string]servers.ServerLocation) ( 13 | data servers.ServerLocation, err error, 14 | ) { 15 | locationData, ok := hostnameToLocation[host] 16 | if !ok { 17 | return locationData, fmt.Errorf("%w: %s", errHostnameNotFound, host) 18 | } 19 | 20 | return locationData, nil 21 | } 22 | 23 | func hostToLocation(locationData []servers.ServerLocation) ( 24 | hostToLocation map[string]servers.ServerLocation, 25 | ) { 26 | hostToLocation = make(map[string]servers.ServerLocation, len(locationData)) 27 | for _, data := range locationData { 28 | hostToLocation[data.Hostname] = data 29 | } 30 | return hostToLocation 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/surfshark/updater/remaining.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/surfshark/servers" 5 | ) 6 | 7 | // getRemainingServers finds extra servers not found in the API or in the ZIP file. 8 | func getRemainingServers(hts hostToServers) { 9 | locationData := servers.LocationData() 10 | hostnameToLocationLeft := hostToLocation(locationData) 11 | for _, hostnameDone := range hts.toHostsSlice() { 12 | delete(hostnameToLocationLeft, hostnameDone) 13 | } 14 | 15 | for hostname, locationData := range hostnameToLocationLeft { 16 | // we assume the OpenVPN server supports both TCP and UDP 17 | const tcp, udp = true, true 18 | hts.addOpenVPN(hostname, locationData.Region, locationData.Country, 19 | locationData.City, locationData.RetroLoc, tcp, udp) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/surfshark/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/surfshark/updater/roundtrip_test.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import "net/http" 4 | 5 | type roundTripFunc func(r *http.Request) (*http.Response, error) 6 | 7 | func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { 8 | return f(r) 9 | } 10 | -------------------------------------------------------------------------------- /internal/provider/surfshark/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | unzipper common.Unzipper 12 | parallelResolver common.ParallelResolver 13 | warner common.Warner 14 | } 15 | 16 | func New(client *http.Client, unzipper common.Unzipper, 17 | warner common.Warner, parallelResolver common.ParallelResolver, 18 | ) *Updater { 19 | return &Updater{ 20 | client: client, 21 | unzipper: unzipper, 22 | parallelResolver: parallelResolver, 23 | warner: warner, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/provider/torguard/connection.go: -------------------------------------------------------------------------------- 1 | package torguard 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(1912, 1912, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/torguard/provider.go: -------------------------------------------------------------------------------- 1 | package torguard 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/torguard/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Torguard 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/torguard/updater/filename.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "strings" 5 | 6 | "golang.org/x/text/cases" 7 | ) 8 | 9 | func parseFilename(fileName string, titleCaser cases.Caser) (country, city string) { 10 | const prefix = "TorGuard." 11 | const suffix = ".ovpn" 12 | s := strings.TrimPrefix(fileName, prefix) 13 | s = strings.TrimSuffix(s, suffix) 14 | 15 | switch { 16 | case strings.Count(s, ".") == 1 && !strings.HasPrefix(s, "USA"): 17 | parts := strings.Split(s, ".") 18 | country = parts[0] 19 | city = parts[1] 20 | 21 | case strings.HasPrefix(s, "USA"): 22 | country = "USA" 23 | s = strings.TrimPrefix(s, "USA-") 24 | s = strings.ReplaceAll(s, "-", " ") 25 | s = strings.ReplaceAll(s, ".", " ") 26 | s = strings.ToLower(s) 27 | s = titleCaser.String(s) 28 | city = s 29 | 30 | default: 31 | country = s 32 | } 33 | 34 | return country, city 35 | } 36 | -------------------------------------------------------------------------------- /internal/provider/torguard/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/torguard/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | parallelResolver common.ParallelResolver 10 | warner common.Warner 11 | } 12 | 13 | func New(unzipper common.Unzipper, warner common.Warner, 14 | parallelResolver common.ParallelResolver, 15 | ) *Updater { 16 | return &Updater{ 17 | unzipper: unzipper, 18 | parallelResolver: parallelResolver, 19 | warner: warner, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/utils/cipher.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func CipherLines(ciphers []string) (lines []string) { 8 | if len(ciphers) == 0 { 9 | return nil 10 | } 11 | 12 | return []string{ 13 | "data-ciphers-fallback " + ciphers[0], 14 | "data-ciphers " + strings.Join(ciphers, ":"), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/provider/utils/cipher_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_CipherLines(t *testing.T) { 10 | t.Parallel() 11 | testCases := map[string]struct { 12 | ciphers []string 13 | version string 14 | lines []string 15 | }{ 16 | "empty version": { 17 | ciphers: []string{"AES"}, 18 | lines: []string{ 19 | "data-ciphers-fallback AES", 20 | "data-ciphers AES", 21 | }, 22 | }, 23 | "2.5": { 24 | ciphers: []string{"AES", "CBC"}, 25 | version: "2.5", 26 | lines: []string{ 27 | "data-ciphers-fallback AES", 28 | "data-ciphers AES:CBC", 29 | }, 30 | }, 31 | } 32 | for name, testCase := range testCases { 33 | t.Run(name, func(t *testing.T) { 34 | t.Parallel() 35 | 36 | lines := CipherLines(testCase.ciphers) 37 | 38 | assert.Equal(t, testCase.lines, lines) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/provider/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Logger interface { 4 | Debug(s string) 5 | Info(s string) 6 | Warn(s string) 7 | Error(s string) 8 | } 9 | -------------------------------------------------------------------------------- /internal/provider/utils/nofetcher.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/qdm12/gluetun/internal/models" 9 | ) 10 | 11 | type NoFetcher struct { 12 | providerName string 13 | } 14 | 15 | func NewNoFetcher(providerName string) *NoFetcher { 16 | return &NoFetcher{ 17 | providerName: providerName, 18 | } 19 | } 20 | 21 | var ErrFetcherNotSupported = errors.New("fetching of servers is not supported") 22 | 23 | func (n *NoFetcher) FetchServers(context.Context, int) ( 24 | servers []models.Server, err error, 25 | ) { 26 | return nil, fmt.Errorf("%w: for %s", ErrFetcherNotSupported, n.providerName) 27 | } 28 | -------------------------------------------------------------------------------- /internal/provider/utils/pick_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/qdm12/gluetun/internal/models" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_pickRandomConnection(t *testing.T) { 12 | t.Parallel() 13 | connections := []models.Connection{ 14 | {Port: 1}, {Port: 2}, {Port: 3}, {Port: 4}, 15 | } 16 | source := rand.NewSource(0) 17 | 18 | connection := pickRandomConnection(connections, source) 19 | assert.Equal(t, models.Connection{Port: 3}, connection) 20 | 21 | connection = pickRandomConnection(connections, source) 22 | assert.Equal(t, models.Connection{Port: 3}, connection) 23 | 24 | connection = pickRandomConnection(connections, source) 25 | assert.Equal(t, models.Connection{Port: 2}, connection) 26 | } 27 | -------------------------------------------------------------------------------- /internal/provider/utils/protocol.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/constants" 6 | "github.com/qdm12/gluetun/internal/constants/vpn" 7 | ) 8 | 9 | func getProtocol(selection settings.ServerSelection) (protocol string) { 10 | if selection.VPN == vpn.OpenVPN && selection.OpenVPN.Protocol == constants.TCP { 11 | return constants.TCP 12 | } 13 | return constants.UDP 14 | } 15 | 16 | func filterByProtocol(selection settings.ServerSelection, 17 | serverTCP, serverUDP bool, 18 | ) (filtered bool) { 19 | switch selection.VPN { 20 | case vpn.Wireguard: 21 | return !serverUDP 22 | default: // OpenVPN 23 | wantTCP := selection.OpenVPN.Protocol == constants.TCP 24 | wantUDP := !wantTCP 25 | return (wantTCP && !serverTCP) || (wantUDP && !serverUDP) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/provider/vpnsecure/connection.go: -------------------------------------------------------------------------------- 1 | package vpnsecure 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(110, 1282, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/vpnsecure/provider.go: -------------------------------------------------------------------------------- 1 | package vpnsecure 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/vpnsecure/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, updaterWarner common.Warner, 20 | parallelResolver common.ParallelResolver, 21 | ) *Provider { 22 | return &Provider{ 23 | storage: storage, 24 | randSource: randSource, 25 | Fetcher: updater.New(client, updaterWarner, parallelResolver), 26 | } 27 | } 28 | 29 | func (p *Provider) Name() string { 30 | return providers.VPNSecure 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/vpnsecure/updater/helpers_test.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "golang.org/x/net/html" 10 | ) 11 | 12 | func parseTestHTML(t *testing.T, htmlString string) *html.Node { 13 | t.Helper() 14 | rootNode, err := html.Parse(strings.NewReader(htmlString)) 15 | require.NoError(t, err) 16 | return rootNode 17 | } 18 | 19 | func parseTestDataIndexHTML(t *testing.T) *html.Node { 20 | t.Helper() 21 | 22 | data, err := os.ReadFile("testdata/index.html") 23 | require.NoError(t, err) 24 | 25 | return parseTestHTML(t, string(data)) 26 | } 27 | -------------------------------------------------------------------------------- /internal/provider/vpnsecure/updater/hosttoserver.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/netip" 5 | 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | type hostToServer map[string]models.Server 10 | 11 | func (hts hostToServer) toHostsSlice() (hosts []string) { 12 | hosts = make([]string, 0, len(hts)) 13 | for host := range hts { 14 | hosts = append(hosts, host) 15 | } 16 | return hosts 17 | } 18 | 19 | func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) { 20 | for host, IPs := range hostToIPs { 21 | server := hts[host] 22 | server.IPs = IPs 23 | hts[host] = server 24 | } 25 | for host, server := range hts { 26 | if len(server.IPs) == 0 { 27 | delete(hts, host) 28 | } 29 | } 30 | } 31 | 32 | func (hts hostToServer) toServersSlice() (servers []models.Server) { 33 | servers = make([]models.Server, 0, len(hts)) 34 | for _, server := range hts { 35 | servers = append(servers, server) 36 | } 37 | return servers 38 | } 39 | -------------------------------------------------------------------------------- /internal/provider/vpnsecure/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxDuration = 5 * time.Second 12 | maxFailRatio = 0.1 13 | maxNoNew = 2 14 | maxFails = 3 15 | ) 16 | return resolver.ParallelSettings{ 17 | Hosts: hosts, 18 | MaxFailRatio: maxFailRatio, 19 | Repeat: resolver.RepeatSettings{ 20 | MaxDuration: maxDuration, 21 | MaxNoNew: maxNoNew, 22 | MaxFails: maxFails, 23 | SortIPs: true, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/provider/vpnsecure/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | parallelResolver common.ParallelResolver 12 | warner common.Warner 13 | } 14 | 15 | func New(client *http.Client, warner common.Warner, 16 | parallelResolver common.ParallelResolver, 17 | ) *Updater { 18 | return &Updater{ 19 | client: client, 20 | parallelResolver: parallelResolver, 21 | warner: warner, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/provider/vpnunlimited/connection.go: -------------------------------------------------------------------------------- 1 | package vpnunlimited 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(1197, 1197, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/vpnunlimited/provider.go: -------------------------------------------------------------------------------- 1 | package vpnunlimited 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/vpnunlimited/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.VPNUnlimited 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/vpnunlimited/updater/hosttoserver.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/netip" 5 | 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | type hostToServer map[string]models.Server 10 | 11 | func (hts hostToServer) toHostsSlice() (hosts []string) { 12 | hosts = make([]string, 0, len(hts)) 13 | for host := range hts { 14 | hosts = append(hosts, host) 15 | } 16 | return hosts 17 | } 18 | 19 | func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) { 20 | for host, IPs := range hostToIPs { 21 | server := hts[host] 22 | server.IPs = IPs 23 | hts[host] = server 24 | } 25 | for host, server := range hts { 26 | if len(server.IPs) == 0 { 27 | delete(hts, host) 28 | } 29 | } 30 | } 31 | 32 | func (hts hostToServer) toServersSlice() (servers []models.Server) { 33 | servers = make([]models.Server, 0, len(hts)) 34 | for _, server := range hts { 35 | servers = append(servers, server) 36 | } 37 | return servers 38 | } 39 | -------------------------------------------------------------------------------- /internal/provider/vpnunlimited/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/vpnunlimited/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | parallelResolver common.ParallelResolver 10 | warner common.Warner 11 | } 12 | 13 | func New(unzipper common.Unzipper, warner common.Warner, 14 | parallelResolver common.ParallelResolver, 15 | ) *Updater { 16 | return &Updater{ 17 | unzipper: unzipper, 18 | parallelResolver: parallelResolver, 19 | warner: warner, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/vyprvpn/connection.go: -------------------------------------------------------------------------------- 1 | package vyprvpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/vyprvpn/provider.go: -------------------------------------------------------------------------------- 1 | package vyprvpn 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/vyprvpn/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | unzipper common.Unzipper, updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Vyprvpn 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/vyprvpn/updater/filename.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var errNotOvpnExt = errors.New("filename does not have the openvpn file extension") 10 | 11 | func parseFilename(fileName string) ( 12 | region string, err error, 13 | ) { 14 | const suffix = ".ovpn" 15 | if !strings.HasSuffix(fileName, suffix) { 16 | return "", fmt.Errorf("%w: %s", errNotOvpnExt, fileName) 17 | } 18 | 19 | region = strings.TrimSuffix(fileName, suffix) 20 | region = strings.ReplaceAll(region, " - ", " ") 21 | return region, nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/provider/vyprvpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 5 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/vyprvpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/provider/common" 5 | ) 6 | 7 | type Updater struct { 8 | unzipper common.Unzipper 9 | parallelResolver common.ParallelResolver 10 | warner common.Warner 11 | } 12 | 13 | func New(unzipper common.Unzipper, warner common.Warner, 14 | parallelResolver common.ParallelResolver, 15 | ) *Updater { 16 | return &Updater{ 17 | unzipper: unzipper, 18 | parallelResolver: parallelResolver, 19 | warner: warner, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/provider/wevpn/connection.go: -------------------------------------------------------------------------------- 1 | package wevpn 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(1195, 1194, 0) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/wevpn/provider.go: -------------------------------------------------------------------------------- 1 | package wevpn 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/qdm12/gluetun/internal/provider/common" 8 | "github.com/qdm12/gluetun/internal/provider/wevpn/updater" 9 | ) 10 | 11 | type Provider struct { 12 | storage common.Storage 13 | randSource rand.Source 14 | common.Fetcher 15 | } 16 | 17 | func New(storage common.Storage, randSource rand.Source, 18 | updaterWarner common.Warner, 19 | parallelResolver common.ParallelResolver, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(updaterWarner, parallelResolver), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Wevpn 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/wevpn/updater/hostname.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import "strings" 4 | 5 | func getHostnameFromCity(city string) (hostname string) { 6 | host := strings.ToLower(city) 7 | host = strings.ReplaceAll(host, ".", "") 8 | host = strings.ReplaceAll(host, " ", "") 9 | 10 | specialCases := map[string]string{ 11 | "washingtondc": "washington", 12 | "mexicocity": "mexico", 13 | "denizli": "bursa", 14 | "sibu": "kualalumpur", 15 | "kiev": "kyiv", 16 | "stpetersburg": "petersburg", 17 | } 18 | if specialHost, ok := specialCases[host]; ok { 19 | host = specialHost 20 | } 21 | 22 | hostname = host + ".wevpn.com" 23 | return hostname 24 | } 25 | -------------------------------------------------------------------------------- /internal/provider/wevpn/updater/resolve.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/qdm12/gluetun/internal/updater/resolver" 7 | ) 8 | 9 | func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { 10 | const ( 11 | maxFailRatio = 0.1 12 | maxDuration = 20 * time.Second 13 | betweenDuration = time.Second 14 | maxNoNew = 2 15 | maxFails = 2 16 | ) 17 | return resolver.ParallelSettings{ 18 | Hosts: hosts, 19 | MaxFailRatio: maxFailRatio, 20 | Repeat: resolver.RepeatSettings{ 21 | MaxDuration: maxDuration, 22 | BetweenDuration: betweenDuration, 23 | MaxNoNew: maxNoNew, 24 | MaxFails: maxFails, 25 | SortIPs: true, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/provider/wevpn/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import "github.com/qdm12/gluetun/internal/provider/common" 4 | 5 | type Updater struct { 6 | parallelResolver common.ParallelResolver 7 | warner common.Warner 8 | } 9 | 10 | func New(warner common.Warner, parallelResolver common.ParallelResolver) *Updater { 11 | return &Updater{ 12 | parallelResolver: parallelResolver, 13 | warner: warner, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/windscribe/connection.go: -------------------------------------------------------------------------------- 1 | package windscribe 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings" 5 | "github.com/qdm12/gluetun/internal/models" 6 | "github.com/qdm12/gluetun/internal/provider/utils" 7 | ) 8 | 9 | func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( 10 | connection models.Connection, err error, 11 | ) { 12 | defaults := utils.NewConnectionDefaults(443, 1194, 1194) //nolint:mnd 13 | return utils.GetConnection(p.Name(), 14 | p.storage, selection, defaults, ipv6Supported, p.randSource) 15 | } 16 | -------------------------------------------------------------------------------- /internal/provider/windscribe/provider.go: -------------------------------------------------------------------------------- 1 | package windscribe 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | 7 | "github.com/qdm12/gluetun/internal/constants/providers" 8 | "github.com/qdm12/gluetun/internal/provider/common" 9 | "github.com/qdm12/gluetun/internal/provider/windscribe/updater" 10 | ) 11 | 12 | type Provider struct { 13 | storage common.Storage 14 | randSource rand.Source 15 | common.Fetcher 16 | } 17 | 18 | func New(storage common.Storage, randSource rand.Source, 19 | client *http.Client, updaterWarner common.Warner, 20 | ) *Provider { 21 | return &Provider{ 22 | storage: storage, 23 | randSource: randSource, 24 | Fetcher: updater.New(client, updaterWarner), 25 | } 26 | } 27 | 28 | func (p *Provider) Name() string { 29 | return providers.Windscribe 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/windscribe/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/qdm12/gluetun/internal/provider/common" 7 | ) 8 | 9 | type Updater struct { 10 | client *http.Client 11 | warner common.Warner 12 | } 13 | 14 | func New(client *http.Client, warner common.Warner) *Updater { 15 | return &Updater{ 16 | client: client, 17 | warner: warner, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/publicip/api/errors.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrTokenNotValid = errors.New("token is not valid") 7 | ErrTooManyRequests = errors.New("too many requests sent for this month") 8 | ErrBadHTTPStatus = errors.New("bad HTTP status received") 9 | ErrServiceLimited = errors.New("service is limited") 10 | ) 11 | -------------------------------------------------------------------------------- /internal/publicip/api/interfaces.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/netip" 6 | 7 | "github.com/qdm12/gluetun/internal/models" 8 | ) 9 | 10 | type Fetcher interface { 11 | String() string 12 | CanFetchAnyIP() bool 13 | Token() (token string) 14 | InfoFetcher 15 | } 16 | 17 | type InfoFetcher interface { 18 | FetchInfo(ctx context.Context, ip netip.Addr) ( 19 | result models.PublicIP, err error) 20 | } 21 | 22 | type Warner interface { 23 | Warn(message string) 24 | } 25 | -------------------------------------------------------------------------------- /internal/publicip/data.go: -------------------------------------------------------------------------------- 1 | package publicip 2 | 3 | import "github.com/qdm12/gluetun/internal/models" 4 | 5 | // GetData returns the public IP data obtained from the last 6 | // fetch. It is notably used by the HTTP control server. 7 | func (l *Loop) GetData() (data models.PublicIP) { 8 | l.ipDataMutex.RLock() 9 | defer l.ipDataMutex.RUnlock() 10 | return l.ipData 11 | } 12 | 13 | // ClearData is used when the VPN connection goes down 14 | // and the public IP is not known anymore. 15 | func (l *Loop) ClearData() (err error) { 16 | l.ipDataMutex.Lock() 17 | defer l.ipDataMutex.Unlock() 18 | l.ipData = models.PublicIP{} 19 | 20 | l.settingsMutex.RLock() 21 | filepath := *l.settings.IPFilepath 22 | l.settingsMutex.RUnlock() 23 | return persistPublicIP(filepath, "", l.puid, l.pgid) 24 | } 25 | -------------------------------------------------------------------------------- /internal/publicip/fs.go: -------------------------------------------------------------------------------- 1 | package publicip 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | ) 7 | 8 | func persistPublicIP(path string, content string, puid, pgid int) error { 9 | const permission = fs.FileMode(0o644) 10 | file, err := os.OpenFile(path, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | _, err = file.WriteString(content) 16 | if err != nil { 17 | _ = file.Close() 18 | return err 19 | } 20 | 21 | if err := file.Chown(puid, pgid); err != nil { 22 | _ = file.Close() 23 | return err 24 | } 25 | 26 | return file.Close() 27 | } 28 | -------------------------------------------------------------------------------- /internal/publicip/interfaces.go: -------------------------------------------------------------------------------- 1 | package publicip 2 | 3 | type Logger interface { 4 | Info(s string) 5 | Warn(s string) 6 | Error(s string) 7 | } 8 | -------------------------------------------------------------------------------- /internal/routing/conversion.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/netip" 7 | ) 8 | 9 | func netIPToNetipAddress(ip net.IP) (address netip.Addr) { 10 | address, ok := netip.AddrFromSlice(ip) 11 | if !ok { 12 | panic(fmt.Sprintf("converting %#v to netip.Addr failed", ip)) 13 | } 14 | return address.Unmap() 15 | } 16 | -------------------------------------------------------------------------------- /internal/routing/errors.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ErrLinkDefaultNotFound = errors.New("default link not found") 8 | -------------------------------------------------------------------------------- /internal/routing/logger.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | type Logger interface { 4 | Debug(s string) 5 | Info(s string) 6 | Warn(s string) 7 | Error(s string) 8 | } 9 | -------------------------------------------------------------------------------- /internal/routing/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | //go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . NetLinker 4 | -------------------------------------------------------------------------------- /internal/server/helpers.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func errMethodNotSupported(w http.ResponseWriter, method string) { 8 | http.Error(w, "method "+method+" not supported", http.StatusBadRequest) 9 | } 10 | 11 | func errRouteNotSupported(w http.ResponseWriter, route string) { 12 | http.Error(w, "route "+route+" not supported", http.StatusBadRequest) 13 | } 14 | -------------------------------------------------------------------------------- /internal/server/interfaces.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/configuration/settings" 7 | "github.com/qdm12/gluetun/internal/models" 8 | ) 9 | 10 | type VPNLooper interface { 11 | GetStatus() (status models.LoopStatus) 12 | ApplyStatus(ctx context.Context, status models.LoopStatus) ( 13 | outcome string, err error) 14 | GetSettings() (settings settings.VPN) 15 | SetSettings(ctx context.Context, settings settings.VPN) (outcome string) 16 | } 17 | 18 | type DNSLoop interface { 19 | ApplyStatus(ctx context.Context, status models.LoopStatus) ( 20 | outcome string, err error) 21 | GetStatus() (status models.LoopStatus) 22 | } 23 | 24 | type PortForwardedGetter interface { 25 | GetPortsForwarded() (ports []uint16) 26 | } 27 | 28 | type PublicIPLoop interface { 29 | GetData() (data models.PublicIP) 30 | } 31 | 32 | type Storage interface { 33 | GetFilterChoices(provider string) models.FilterChoices 34 | } 35 | -------------------------------------------------------------------------------- /internal/server/logger.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type Logger interface { 4 | Debugf(format string, args ...any) 5 | infoer 6 | warner 7 | Warnf(format string, args ...any) 8 | errorer 9 | } 10 | 11 | type infoWarner interface { 12 | infoer 13 | warner 14 | } 15 | 16 | type infoer interface { 17 | Info(s string) 18 | } 19 | 20 | type warner interface { 21 | Warn(s string) 22 | } 23 | 24 | type errorer interface { 25 | Error(s string) 26 | } 27 | -------------------------------------------------------------------------------- /internal/server/middlewares/auth/apikey.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/sha256" 5 | "crypto/subtle" 6 | "net/http" 7 | ) 8 | 9 | type apiKeyMethod struct { 10 | apiKeyDigest [32]byte 11 | } 12 | 13 | func newAPIKeyMethod(apiKey string) *apiKeyMethod { 14 | return &apiKeyMethod{ 15 | apiKeyDigest: sha256.Sum256([]byte(apiKey)), 16 | } 17 | } 18 | 19 | // equal returns true if another auth checker is equal. 20 | // This is used to deduplicate checkers for a particular route. 21 | func (a *apiKeyMethod) equal(other authorizationChecker) bool { 22 | otherTokenMethod, ok := other.(*apiKeyMethod) 23 | if !ok { 24 | return false 25 | } 26 | return a.apiKeyDigest == otherTokenMethod.apiKeyDigest 27 | } 28 | 29 | func (a *apiKeyMethod) isAuthorized(_ http.Header, request *http.Request) bool { 30 | xAPIKey := request.Header.Get("X-API-Key") 31 | if xAPIKey == "" { 32 | xAPIKey = request.URL.Query().Get("api_key") 33 | } 34 | xAPIKeyDigest := sha256.Sum256([]byte(xAPIKey)) 35 | return subtle.ConstantTimeCompare(xAPIKeyDigest[:], a.apiKeyDigest[:]) == 1 36 | } 37 | -------------------------------------------------------------------------------- /internal/server/middlewares/auth/configfile.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/pelletier/go-toml/v2" 9 | ) 10 | 11 | // Read reads the toml file specified by the filepath given. 12 | // If the file does not exist, it returns empty settings and no error. 13 | func Read(filepath string) (settings Settings, err error) { 14 | file, err := os.Open(filepath) 15 | if err != nil { 16 | if errors.Is(err, os.ErrNotExist) { 17 | return Settings{}, nil 18 | } 19 | return settings, fmt.Errorf("opening file: %w", err) 20 | } 21 | decoder := toml.NewDecoder(file) 22 | decoder.DisallowUnknownFields() 23 | err = decoder.Decode(&settings) 24 | if err == nil { 25 | return settings, nil 26 | } 27 | 28 | strictErr := new(toml.StrictMissingError) 29 | ok := errors.As(err, &strictErr) 30 | if !ok { 31 | return settings, fmt.Errorf("toml decoding file: %w", err) 32 | } 33 | return settings, fmt.Errorf("toml decoding file: %w:\n%s", 34 | strictErr, strictErr.String()) 35 | } 36 | -------------------------------------------------------------------------------- /internal/server/middlewares/auth/format.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | func andStrings(strings []string) (result string) { 4 | return joinStrings(strings, "and") 5 | } 6 | 7 | func joinStrings(strings []string, lastJoin string) (result string) { 8 | if len(strings) == 0 { 9 | return "" 10 | } 11 | 12 | result = strings[0] 13 | for i := 1; i < len(strings); i++ { 14 | if i < len(strings)-1 { 15 | result += ", " + strings[i] 16 | } else { 17 | result += " " + lastJoin + " " + strings[i] 18 | } 19 | } 20 | 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /internal/server/middlewares/auth/interfaces.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | type DebugLogger interface { 4 | Debugf(format string, args ...any) 5 | Warnf(format string, args ...any) 6 | } 7 | -------------------------------------------------------------------------------- /internal/server/middlewares/auth/interfaces_local.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "net/http" 4 | 5 | type authorizationChecker interface { 6 | equal(other authorizationChecker) bool 7 | isAuthorized(headers http.Header, request *http.Request) bool 8 | } 9 | -------------------------------------------------------------------------------- /internal/server/middlewares/auth/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | //go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . DebugLogger 4 | -------------------------------------------------------------------------------- /internal/server/middlewares/auth/none.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "net/http" 4 | 5 | type noneMethod struct{} 6 | 7 | func newNoneMethod() *noneMethod { 8 | return &noneMethod{} 9 | } 10 | 11 | // equal returns true if another auth checker is equal. 12 | // This is used to deduplicate checkers for a particular route. 13 | func (n *noneMethod) equal(other authorizationChecker) bool { 14 | _, ok := other.(*noneMethod) 15 | return ok 16 | } 17 | 18 | func (n *noneMethod) isAuthorized(_ http.Header, _ *http.Request) bool { 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /internal/server/middlewares/log/interfaces.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | type Logger interface { 4 | Info(message string) 5 | } 6 | -------------------------------------------------------------------------------- /internal/server/publicip.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func newPublicIPHandler(loop PublicIPLoop, w warner) http.Handler { 10 | return &publicIPHandler{ 11 | loop: loop, 12 | warner: w, 13 | } 14 | } 15 | 16 | type publicIPHandler struct { 17 | loop PublicIPLoop 18 | warner warner 19 | } 20 | 21 | func (h *publicIPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 22 | r.RequestURI = strings.TrimPrefix(r.RequestURI, "/publicip") 23 | switch r.RequestURI { 24 | case "/ip": 25 | switch r.Method { 26 | case http.MethodGet: 27 | h.getPublicIP(w) 28 | default: 29 | errMethodNotSupported(w, r.Method) 30 | } 31 | default: 32 | errRouteNotSupported(w, r.RequestURI) 33 | } 34 | } 35 | 36 | func (h *publicIPHandler) getPublicIP(w http.ResponseWriter) { 37 | data := h.loop.GetData() 38 | encoder := json.NewEncoder(w) 39 | if err := encoder.Encode(data); err != nil { 40 | h.warner.Warn(err.Error()) 41 | w.WriteHeader(http.StatusInternalServerError) 42 | return 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/server/wrappers.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/qdm12/gluetun/internal/constants" 8 | "github.com/qdm12/gluetun/internal/models" 9 | ) 10 | 11 | type statusWrapper struct { 12 | Status string `json:"status"` 13 | } 14 | 15 | var errInvalidStatus = errors.New("invalid status") 16 | 17 | func (sw *statusWrapper) getStatus() (status models.LoopStatus, err error) { 18 | status = models.LoopStatus(sw.Status) 19 | switch status { 20 | case constants.Stopped, constants.Running: 21 | return status, nil 22 | default: 23 | return "", fmt.Errorf("%w: %s: possible values are: %s, %s", 24 | errInvalidStatus, sw.Status, constants.Stopped, constants.Running) 25 | } 26 | } 27 | 28 | type portWrapper struct { // TODO v4 remove 29 | Port uint16 `json:"port"` 30 | } 31 | 32 | type portsWrapper struct { 33 | Ports []uint16 `json:"ports"` 34 | } 35 | 36 | type outcomeWrapper struct { 37 | Outcome string `json:"outcome"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/shadowsocks/logger.go: -------------------------------------------------------------------------------- 1 | package shadowsocks 2 | 3 | type Logger interface { 4 | debuger 5 | infoer 6 | errorer 7 | } 8 | 9 | type debuger interface { 10 | Debug(s string) 11 | } 12 | 13 | type infoer interface { 14 | Info(s string) 15 | } 16 | 17 | type errorer interface { 18 | Error(s string) 19 | } 20 | -------------------------------------------------------------------------------- /internal/storage/choices.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/qdm12/gluetun/internal/configuration/settings/validation" 5 | "github.com/qdm12/gluetun/internal/constants/providers" 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | func (s *Storage) GetFilterChoices(provider string) models.FilterChoices { 10 | if provider == providers.Custom { 11 | return models.FilterChoices{} 12 | } 13 | 14 | s.mergedMutex.RLock() 15 | defer s.mergedMutex.RUnlock() 16 | 17 | serversObject := s.getMergedServersObject(provider) 18 | servers := serversObject.Servers 19 | return models.FilterChoices{ 20 | Countries: validation.ExtractCountries(servers), 21 | Categories: validation.ExtractCategories(servers), 22 | Regions: validation.ExtractRegions(servers), 23 | Cities: validation.ExtractCities(servers), 24 | ISPs: validation.ExtractISPs(servers), 25 | Names: validation.ExtractServerNames(servers), 26 | Hostnames: validation.ExtractHostnames(servers), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/storage/copy.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "net/netip" 5 | 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | func copyServer(server models.Server) (serverCopy models.Server) { 10 | serverCopy = server 11 | serverCopy.IPs = copyIPs(server.IPs) 12 | return serverCopy 13 | } 14 | 15 | func copyIPs(toCopy []netip.Addr) (copied []netip.Addr) { 16 | if toCopy == nil { 17 | return nil 18 | } 19 | 20 | copied = make([]netip.Addr, len(toCopy)) 21 | copy(copied, toCopy) 22 | return copied 23 | } 24 | -------------------------------------------------------------------------------- /internal/storage/hardcoded.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | 7 | "github.com/qdm12/gluetun/internal/models" 8 | ) 9 | 10 | //go:embed servers.json 11 | var allServersEmbedFS embed.FS 12 | 13 | func parseHardcodedServers() (allServers models.AllServers, err error) { 14 | f, err := allServersEmbedFS.Open("servers.json") 15 | if err != nil { 16 | return allServers, err 17 | } 18 | decoder := json.NewDecoder(f) 19 | err = decoder.Decode(&allServers) 20 | return allServers, err 21 | } 22 | -------------------------------------------------------------------------------- /internal/storage/hardcoded_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/qdm12/gluetun/internal/constants/providers" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_parseHardcodedServers(t *testing.T) { 12 | t.Parallel() 13 | 14 | servers, err := parseHardcodedServers() 15 | 16 | require.NoError(t, err) 17 | 18 | // all providers minus custom 19 | allProviders := providers.All() 20 | require.Equal(t, len(allProviders), len(servers.ProviderToServers)) 21 | for _, provider := range allProviders { 22 | servers, ok := servers.ProviderToServers[provider] 23 | assert.Truef(t, ok, "for provider %s", provider) 24 | assert.NotEmptyf(t, servers, "for provider %s", provider) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/storage/helpers.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "fmt" 4 | 5 | func panicOnProviderMissingHardcoded(provider string) { 6 | panic(fmt.Sprintf("provider %s not found in hardcoded servers map; "+ 7 | "did you add the provider key in the embedded servers.json?", provider)) 8 | } 9 | -------------------------------------------------------------------------------- /internal/storage/mocks_generate_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | //go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Infoer 4 | -------------------------------------------------------------------------------- /internal/tun/check_unspecified.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin 2 | 3 | package tun 4 | 5 | func (t *Tun) Check(path string) error { 6 | panic("not implemented") 7 | } 8 | -------------------------------------------------------------------------------- /internal/tun/create_unspecified.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin 2 | 3 | package tun 4 | 5 | // Create creates a TUN device at the path specified. 6 | func (t *Tun) Create(path string) error { 7 | panic("not implemented") 8 | } 9 | -------------------------------------------------------------------------------- /internal/tun/tun.go: -------------------------------------------------------------------------------- 1 | package tun 2 | 3 | type Tun struct{} 4 | 5 | func New() *Tun { 6 | return &Tun{} 7 | } 8 | -------------------------------------------------------------------------------- /internal/updater/html/attribute.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import "golang.org/x/net/html" 4 | 5 | func Attribute(node *html.Node, key string) (value string) { 6 | for _, attribute := range node.Attr { 7 | if attribute.Key == key { 8 | return attribute.Val 9 | } 10 | } 11 | return "" 12 | } 13 | -------------------------------------------------------------------------------- /internal/updater/html/bfs.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "container/list" 5 | "fmt" 6 | 7 | "golang.org/x/net/html" 8 | ) 9 | 10 | // BFS returns the node matching the match function and nil 11 | // if no node is found. 12 | func BFS(rootNode *html.Node, match MatchFunc) (node *html.Node) { 13 | visited := make(map[*html.Node]struct{}) 14 | queue := list.New() 15 | _ = queue.PushBack(rootNode) 16 | 17 | for queue.Len() > 0 { 18 | listElement := queue.Front() 19 | node, ok := queue.Remove(listElement).(*html.Node) 20 | if !ok { 21 | panic(fmt.Sprintf("linked list has bad type %T", listElement.Value)) 22 | } 23 | 24 | if node == nil { 25 | continue 26 | } 27 | 28 | if _, ok := visited[node]; ok { 29 | continue 30 | } 31 | visited[node] = struct{}{} 32 | 33 | if match(node) { 34 | return node 35 | } 36 | 37 | for child := node.FirstChild; child != nil; child = child.NextSibling { 38 | _ = queue.PushBack(child) 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/updater/html/css.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "strings" 5 | 6 | "golang.org/x/net/html" 7 | ) 8 | 9 | func HasClassStrings(node *html.Node, classStrings ...string) (match bool) { 10 | targetClasses := make(map[string]struct{}, len(classStrings)) 11 | for _, classString := range classStrings { 12 | targetClasses[classString] = struct{}{} 13 | } 14 | 15 | classAttribute := Attribute(node, "class") 16 | classes := strings.Fields(classAttribute) 17 | for _, class := range classes { 18 | delete(targetClasses, class) 19 | } 20 | 21 | return len(targetClasses) == 0 22 | } 23 | -------------------------------------------------------------------------------- /internal/updater/html/errors.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "golang.org/x/net/html" 8 | ) 9 | 10 | func WrapError(sentinelError error, node *html.Node) error { 11 | return fmt.Errorf("%w: in HTML code: %s", 12 | sentinelError, mustRenderHTML(node)) 13 | } 14 | 15 | func WrapWarning(warning string, node *html.Node) string { 16 | return fmt.Sprintf("%s: in HTML code: %s", 17 | warning, mustRenderHTML(node)) 18 | } 19 | 20 | func mustRenderHTML(node *html.Node) (rendered string) { 21 | stringBuffer := bytes.NewBufferString("") 22 | err := html.Render(stringBuffer, node) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return stringBuffer.String() 27 | } 28 | -------------------------------------------------------------------------------- /internal/updater/html/fetch.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | "golang.org/x/net/html" 10 | ) 11 | 12 | var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code is not OK") 13 | 14 | func Fetch(ctx context.Context, client *http.Client, url string) ( 15 | rootNode *html.Node, err error, 16 | ) { 17 | request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 18 | if err != nil { 19 | return nil, fmt.Errorf("creating HTTP request: %w", err) 20 | } 21 | 22 | response, err := client.Do(request) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | if response.StatusCode != http.StatusOK { 28 | return nil, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK, 29 | response.StatusCode, response.Status) 30 | } 31 | 32 | rootNode, err = html.Parse(response.Body) 33 | if err != nil { 34 | _ = response.Body.Close() 35 | return nil, fmt.Errorf("parsing HTML code: %w", err) 36 | } 37 | 38 | err = response.Body.Close() 39 | if err != nil { 40 | return nil, fmt.Errorf("closing response body: %w", err) 41 | } 42 | 43 | return rootNode, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/updater/html/match.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "golang.org/x/net/html" 5 | ) 6 | 7 | type MatchFunc func(node *html.Node) (match bool) 8 | 9 | func MatchID(id string) MatchFunc { 10 | return func(node *html.Node) (match bool) { 11 | if node == nil { 12 | return false 13 | } 14 | 15 | return Attribute(node, "id") == id 16 | } 17 | } 18 | 19 | func MatchData(data string) MatchFunc { 20 | return func(node *html.Node) (match bool) { 21 | return node != nil && node.Type == html.ElementNode && node.Data == data 22 | } 23 | } 24 | 25 | func DirectChild(parent *html.Node, 26 | matchFunc MatchFunc, 27 | ) (child *html.Node) { 28 | for child := parent.FirstChild; child != nil; child = child.NextSibling { 29 | if matchFunc(child) { 30 | return child 31 | } 32 | } 33 | return nil 34 | } 35 | 36 | func DirectChildren(parent *html.Node, 37 | matchFunc MatchFunc, 38 | ) (children []*html.Node) { 39 | for child := parent.FirstChild; child != nil; child = child.NextSibling { 40 | if matchFunc(child) { 41 | children = append(children, child) 42 | } 43 | } 44 | return children 45 | } 46 | -------------------------------------------------------------------------------- /internal/updater/interfaces.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/configuration/settings" 7 | "github.com/qdm12/gluetun/internal/models" 8 | "github.com/qdm12/gluetun/internal/provider" 9 | ) 10 | 11 | type Providers interface { 12 | Get(providerName string) provider.Provider 13 | } 14 | 15 | type Storage interface { 16 | SetServers(provider string, servers []models.Server) (err error) 17 | GetServersCount(provider string) (count int) 18 | ServersAreEqual(provider string, servers []models.Server) (equal bool) 19 | // Extra methods to match the provider.New storage interface 20 | FilterServers(provider string, selection settings.ServerSelection) (filtered []models.Server, err error) 21 | } 22 | 23 | type Unzipper interface { 24 | FetchAndExtract(ctx context.Context, url string) ( 25 | contents map[string][]byte, err error) 26 | } 27 | 28 | type Logger interface { 29 | Info(s string) 30 | Warn(s string) 31 | Error(s string) 32 | } 33 | -------------------------------------------------------------------------------- /internal/updater/openvpn/fetch.go: -------------------------------------------------------------------------------- 1 | package openvpn 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | func FetchFile(ctx context.Context, client *http.Client, url string) ( 11 | host string, err error, 12 | ) { 13 | b, err := fetchData(ctx, client, url) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | const rejectIP = true 19 | const rejectDomain = false 20 | hosts := extractRemoteHosts(b, rejectIP, rejectDomain) 21 | if len(hosts) == 0 { 22 | return "", fmt.Errorf("%w for url %s", ErrNoRemoteHost, url) 23 | } 24 | 25 | return hosts[0], nil 26 | } 27 | 28 | func fetchData(ctx context.Context, client *http.Client, url string) ( 29 | b []byte, err error, 30 | ) { 31 | request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | response, err := client.Do(request) 37 | if err != nil { 38 | return nil, err 39 | } 40 | defer response.Body.Close() 41 | 42 | return io.ReadAll(response.Body) 43 | } 44 | -------------------------------------------------------------------------------- /internal/updater/resolver/ips.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "net/netip" 5 | ) 6 | 7 | func uniqueIPsToSlice(uniqueIPs map[string]struct{}) (ips []netip.Addr) { 8 | ips = make([]netip.Addr, 0, len(uniqueIPs)) 9 | for key := range uniqueIPs { 10 | ip, err := netip.ParseAddr(key) 11 | if err != nil { 12 | panic(err) 13 | } 14 | if ip.Is4In6() { 15 | ip = netip.AddrFrom4(ip.As4()) 16 | } 17 | ips = append(ips, ip) 18 | } 19 | return ips 20 | } 21 | -------------------------------------------------------------------------------- /internal/updater/resolver/net.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | func newResolver(resolverAddress string) *net.Resolver { 9 | d := net.Dialer{} 10 | resolverAddress = net.JoinHostPort(resolverAddress, "53") 11 | return &net.Resolver{ 12 | PreferGo: true, 13 | Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { 14 | return d.DialContext(ctx, "udp", resolverAddress) 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/updater/unzip/extract.go: -------------------------------------------------------------------------------- 1 | package unzip 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "io" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | func zipExtractAll(zipBytes []byte) (contents map[string][]byte, err error) { 12 | r, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) 13 | if err != nil { 14 | return nil, err 15 | } 16 | contents = map[string][]byte{} 17 | for _, zf := range r.File { 18 | fileName := filepath.Base(zf.Name) 19 | if !strings.HasSuffix(fileName, ".ovpn") && 20 | !strings.HasSuffix(fileName, ".conf") { 21 | continue 22 | } 23 | f, err := zf.Open() 24 | if err != nil { 25 | return nil, err 26 | } 27 | defer f.Close() 28 | contents[fileName], err = io.ReadAll(f) 29 | if err != nil { 30 | return nil, err 31 | } 32 | if err := f.Close(); err != nil { 33 | return nil, err 34 | } 35 | } 36 | return contents, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/updater/unzip/fetch.go: -------------------------------------------------------------------------------- 1 | package unzip 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK") 12 | 13 | func (u *Unzipper) FetchAndExtract(ctx context.Context, url string) ( 14 | contents map[string][]byte, err error, 15 | ) { 16 | request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 17 | if err != nil { 18 | return nil, err 19 | } 20 | request.Header.Set("User-Agent", "gluetun") 21 | 22 | response, err := u.client.Do(request) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer response.Body.Close() 27 | 28 | if response.StatusCode != http.StatusOK { 29 | return nil, fmt.Errorf("%w: %s: %d %s", ErrHTTPStatusCodeNotOK, 30 | url, response.StatusCode, response.Status) 31 | } 32 | 33 | b, err := io.ReadAll(response.Body) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | if err := response.Body.Close(); err != nil { 39 | return nil, err 40 | } 41 | 42 | return zipExtractAll(b) 43 | } 44 | -------------------------------------------------------------------------------- /internal/updater/unzip/unzip.go: -------------------------------------------------------------------------------- 1 | package unzip 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Unzipper struct { 8 | client *http.Client 9 | } 10 | 11 | func New(client *http.Client) *Unzipper { 12 | return &Unzipper{ 13 | client: client, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/vpn/cleanup.go: -------------------------------------------------------------------------------- 1 | package vpn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | func (l *Loop) cleanup() { 9 | for _, vpnPort := range l.vpnInputPorts { 10 | err := l.fw.RemoveAllowedPort(context.Background(), vpnPort) 11 | if err != nil { 12 | l.logger.Error("cannot remove allowed input port from firewall: " + err.Error()) 13 | } 14 | } 15 | 16 | err := l.publicip.ClearData() 17 | if err != nil { 18 | l.logger.Error("clearing public IP data: " + err.Error()) 19 | } 20 | 21 | err = l.stopPortForwarding() 22 | if err != nil { 23 | portForwardingAlreadyStopped := errors.Is(err, context.Canceled) 24 | if !portForwardingAlreadyStopped { 25 | l.logger.Error("stopping port forwarding: " + err.Error()) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/vpn/settings.go: -------------------------------------------------------------------------------- 1 | package vpn 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/configuration/settings" 7 | ) 8 | 9 | func (l *Loop) GetSettings() (settings settings.VPN) { 10 | return l.state.GetSettings() 11 | } 12 | 13 | func (l *Loop) SetSettings(ctx context.Context, 14 | vpn settings.VPN) ( 15 | outcome string, 16 | ) { 17 | return l.state.SetSettings(ctx, vpn) 18 | } 19 | -------------------------------------------------------------------------------- /internal/vpn/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/qdm12/gluetun/internal/configuration/settings" 8 | "github.com/qdm12/gluetun/internal/models" 9 | ) 10 | 11 | func New(statusApplier StatusApplier, vpn settings.VPN) *State { 12 | return &State{ 13 | statusApplier: statusApplier, 14 | vpn: vpn, 15 | } 16 | } 17 | 18 | type State struct { 19 | statusApplier StatusApplier 20 | 21 | vpn settings.VPN 22 | settingsMu sync.RWMutex 23 | } 24 | 25 | type StatusApplier interface { 26 | ApplyStatus(ctx context.Context, status models.LoopStatus) ( 27 | outcome string, err error) 28 | } 29 | -------------------------------------------------------------------------------- /internal/vpn/state/vpn.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | "github.com/qdm12/gluetun/internal/configuration/settings" 8 | "github.com/qdm12/gluetun/internal/constants" 9 | ) 10 | 11 | func (s *State) GetSettings() (vpn settings.VPN) { 12 | s.settingsMu.RLock() 13 | vpn = s.vpn.Copy() 14 | s.settingsMu.RUnlock() 15 | return vpn 16 | } 17 | 18 | func (s *State) SetSettings(ctx context.Context, vpn settings.VPN) ( 19 | outcome string, 20 | ) { 21 | s.settingsMu.Lock() 22 | settingsUnchanged := reflect.DeepEqual(s.vpn, vpn) 23 | if settingsUnchanged { 24 | s.settingsMu.Unlock() 25 | return "settings left unchanged" 26 | } 27 | s.vpn = vpn 28 | s.settingsMu.Unlock() 29 | _, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped) 30 | outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running) 31 | return outcome 32 | } 33 | -------------------------------------------------------------------------------- /internal/vpn/status.go: -------------------------------------------------------------------------------- 1 | package vpn 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/qdm12/gluetun/internal/models" 7 | ) 8 | 9 | func (l *Loop) GetStatus() (status models.LoopStatus) { 10 | return l.statusManager.GetStatus() 11 | } 12 | 13 | func (l *Loop) ApplyStatus(ctx context.Context, status models.LoopStatus) ( 14 | outcome string, err error, 15 | ) { 16 | return l.statusManager.ApplyStatus(ctx, status) 17 | } 18 | -------------------------------------------------------------------------------- /internal/wireguard/address.go: -------------------------------------------------------------------------------- 1 | package wireguard 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | 7 | "github.com/qdm12/gluetun/internal/netlink" 8 | ) 9 | 10 | func (w *Wireguard) addAddresses(link netlink.Link, 11 | addresses []netip.Prefix, 12 | ) (err error) { 13 | for _, ipNet := range addresses { 14 | if !*w.settings.IPv6 && ipNet.Addr().Is6() { 15 | continue 16 | } 17 | 18 | address := netlink.Addr{ 19 | Network: ipNet, 20 | } 21 | 22 | err = w.netlink.AddrReplace(link, address) 23 | if err != nil { 24 | return fmt.Errorf("%w: when adding address %s to link %s", 25 | err, address, link.Name) 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/wireguard/constructor.go: -------------------------------------------------------------------------------- 1 | package wireguard 2 | 3 | type Wireguard struct { 4 | logger Logger 5 | settings Settings 6 | netlink NetLinker 7 | } 8 | 9 | func New(settings Settings, netlink NetLinker, 10 | logger Logger, 11 | ) (w *Wireguard, err error) { 12 | settings.SetDefaults() 13 | if err := settings.Check(); err != nil { 14 | return nil, err 15 | } 16 | 17 | return &Wireguard{ 18 | logger: logger, 19 | settings: settings, 20 | netlink: netlink, 21 | }, nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/wireguard/helpers_test.go: -------------------------------------------------------------------------------- 1 | package wireguard 2 | 3 | func ptrTo[T any](x T) *T { return &x } 4 | -------------------------------------------------------------------------------- /internal/wireguard/log.go: -------------------------------------------------------------------------------- 1 | package wireguard 2 | 3 | import ( 4 | "golang.zx2c4.com/wireguard/device" 5 | ) 6 | 7 | //go:generate mockgen -destination=log_mock_test.go -package wireguard . Logger 8 | 9 | type Logger interface { 10 | Debug(s string) 11 | Debugf(format string, args ...interface{}) 12 | Info(s string) 13 | Error(s string) 14 | Errorf(format string, args ...interface{}) 15 | } 16 | 17 | func makeDeviceLogger(logger Logger) (deviceLogger *device.Logger) { 18 | return &device.Logger{ 19 | Verbosef: logger.Debugf, 20 | Errorf: logger.Errorf, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/wireguard/log_test.go: -------------------------------------------------------------------------------- 1 | package wireguard 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | ) 8 | 9 | func Test_makeDeviceLogger(t *testing.T) { 10 | t.Parallel() 11 | 12 | ctrl := gomock.NewController(t) 13 | 14 | logger := NewMockLogger(ctrl) 15 | 16 | deviceLogger := makeDeviceLogger(logger) 17 | 18 | logger.EXPECT().Debugf("test %d", 1) 19 | deviceLogger.Verbosef("test %d", 1) 20 | 21 | logger.EXPECT().Errorf("test %d", 2) 22 | deviceLogger.Errorf("test %d", 2) 23 | } 24 | -------------------------------------------------------------------------------- /internal/wireguard/netlinker.go: -------------------------------------------------------------------------------- 1 | package wireguard 2 | 3 | import "github.com/qdm12/gluetun/internal/netlink" 4 | 5 | //go:generate mockgen -destination=netlinker_mock_test.go -package wireguard . NetLinker 6 | 7 | type NetLinker interface { 8 | AddrReplace(link netlink.Link, addr netlink.Addr) error 9 | Router 10 | Ruler 11 | Linker 12 | IsWireguardSupported() (ok bool, err error) 13 | } 14 | 15 | type Router interface { 16 | RouteList(family int) (routes []netlink.Route, err error) 17 | RouteAdd(route netlink.Route) error 18 | } 19 | 20 | type Ruler interface { 21 | RuleAdd(rule netlink.Rule) error 22 | RuleDel(rule netlink.Rule) error 23 | } 24 | 25 | type Linker interface { 26 | LinkAdd(link netlink.Link) (linkIndex int, err error) 27 | LinkList() (links []netlink.Link, err error) 28 | LinkByName(name string) (link netlink.Link, err error) 29 | LinkSetUp(link netlink.Link) (linkIndex int, err error) 30 | LinkSetDown(link netlink.Link) error 31 | LinkDel(link netlink.Link) error 32 | } 33 | -------------------------------------------------------------------------------- /internal/wireguard/rule.go: -------------------------------------------------------------------------------- 1 | package wireguard 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/qdm12/gluetun/internal/netlink" 8 | ) 9 | 10 | func (w *Wireguard) addRule(rulePriority int, firewallMark uint32, 11 | family int, 12 | ) (cleanup func() error, err error) { 13 | rule := netlink.NewRule() 14 | rule.Invert = true 15 | rule.Priority = rulePriority 16 | rule.Mark = firewallMark 17 | rule.Table = int(firewallMark) 18 | rule.Family = family 19 | if err := w.netlink.RuleAdd(rule); err != nil { 20 | if strings.HasSuffix(err.Error(), "file exists") { 21 | w.logger.Info("if you are using Kubernetes, this may fix the error below: " + 22 | "https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/kubernetes.md#adding-ipv6-rule--file-exists") 23 | } 24 | return nil, fmt.Errorf("adding %s: %w", rule, err) 25 | } 26 | 27 | cleanup = func() error { 28 | err := w.netlink.RuleDel(rule) 29 | if err != nil { 30 | return fmt.Errorf("deleting rule %s: %w", rule, err) 31 | } 32 | return nil 33 | } 34 | return cleanup, nil 35 | } 36 | --------------------------------------------------------------------------------