Skip to content

Commit b9ef1c7

Browse files
committed
**v0.5.0**
- Set default best score to -1, to catch cases where all available options score 0 (resulting in no connection added) + raised load percentage to 95% to yield a 0 score - Custom DNS servers via settings is now possible - Correct Onion over VPN -> Onion Over VPN (chadsr#85) - Experimental 'slow-mode' (sync -s) - Live progress during benchmarking - RPM package fix for wrongly used library directory - Executable now installed to /usr/bin instead of /usr/local/bin - Settings/input case fixes (everything is converted to lower-case)
1 parent c15d9db commit b9ef1c7

File tree

6 files changed

+59
-26
lines changed

6 files changed

+59
-26
lines changed

.travis.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ jobs:
5555
- DEB_PYTHON=python3
5656
- DEB_PIP=pip3
5757
- FED_DOCKER=fedora:latest
58-
- FED_PYTHON=python3
59-
- FED_PIP=pip3
58+
- FED_PYTHON=python3.6
59+
- FED_PIP=pip3.6
6060
- JFROG_USER=chadsr
6161
- JFROG_CLI_OFFER_CONFIG=false
6262
- secure: "pPG85EvUVWahXHSVToXJ9yE/ow4U1mC/RX63J3mjSG5Ywp7BYqdNEa9/YD2cwZaDx1FiwHmbSHZWiYbmNkVKfI0CgcrGXCgKLeRusAHqCSAQ7TsDbk8/LW2e/5saSDqyE7dDvZxyktsJgn906cWu2ZcX+GnwC3sEVW2GNqVIxkRcMqjoTCgxewyodnOMurbSTRrIpVlgVsXu1nBhXvocatFNXE1/RhJnE43KBgVIZHDgGysdZl2RHYkMYy4oW3g80f/d2zrwvQhnlTezUWgx6TGjpfD6w+3JnwrlVG/17A76NBu2GT3YB545hrxPw7QikZ2KMYxJ2TEKN92KResclK8wWQx1IT1csikRYwkXIPESJxzAkQfPNBqmjOofekawc7XOzsC+btOOpJz1CEvksaXfaepGJOmJr2N7h7ZeMRI8I2sXicTsvL7/9R5Tbb5wDOtnmt0PiHMyHBGMF9sBwi64qjQGoq0bj6Q0AxIbXthd2v/p5BDHSA+7fDWUuLpmRyqiPuFZckg+Cd9Q1VsldcR8Qy3pkHMwhyKK4QBgqxsEAgCTFoAozkB9E8ICgE28lJwdk7aQ5Y24ynwcbUK8Jz981v5IlkvJs6vXD6jw1tEo3YNYvi0M5FBcwkH0FNSbEVu0U1dM75wNKjJpXPIX3FBF6+IcQE/K7qXgkT3lU4k="
@@ -76,8 +76,8 @@ jobs:
7676
- mkdir builds && cd builds/
7777
# Start building packages
7878
- echo "Deploying ${PACKAGE} v${VERSION} from PyPi to ${GH_USER}/${NAME} - ${DESCRIPTION}"
79-
- docker run -e PACKAGE_DIR=${PACKAGE_DIR} -e DEB_PYTHON=$DEB_PYTHON -e DEB_PIP=$DEB_PIP -e PYTHON_MIN_VERSION=$PYTHON_MIN_VERSION -e DESCRIPTION="${DESCRIPTION}" -e PACKAGE=$PACKAGE -e GH_USER=$GH_USER -e NAME=$NAME -v $PWD:/mnt/travis ${DEB_DOCKER} /bin/bash -c 'su && cd /mnt/travis && apt-get update && apt-get -y install ${DEB_PYTHON} ${DEB_PYTHON}-pip ruby ruby-dev rubygems-integration build-essential && gem install --no-ri --no-rdoc fpm && ${DEB_PYTHON} --version && fpm --maintainer "${GH_USER}" --description "${DESCRIPTION}" --prefix="${PACKAGE_DIR}" --url "https://github.com/${GH_USER}/${NAME}" --provides "${PACKAGE}" -a "all" --no-python-fix-name --python-package-name-prefix "${DEB_PYTHON}" --python-bin ${DEB_PYTHON} --python-pip ${DEB_PIP} --python-install-lib="lib/${DEB_PYTHON}/dist-packages/" -d "${DEB_PYTHON} >= ${PYTHON_MIN_VERSION}" -d iputils-ping -d "network-manager >= 1.2.0" -d openvpn -d network-manager-openvpn-gnome -s python -t deb ${PACKAGE}'
80-
- docker run -e PACKAGE_DIR=${PACKAGE_DIR} -e FED_PYTHON=$FED_PYTHON -e FED_PIP=$FED_PIP -e DESCRIPTION="${DESCRIPTION}" -e PACKAGE=$PACKAGE -e GH_USER=$GH_USER -e NAME=$NAME -v $PWD:/mnt/travis ${FED_DOCKER} /bin/bash -c 'su && cd /mnt/travis && dnf install ruby-devel gcc make rpm-build libffi-devel ${FED_PYTHON} ${FED_PYTHON}-pip -y && gem install --no-ri --no-rdoc fpm && ${FED_PYTHON} --version && fpm --maintainer "${GH_USER}" --description "${DESCRIPTION}" --prefix="${PACKAGE_DIR}" --url "https://github.com/${GH_USER}/${NAME}" --provides "${PACKAGE}" -a "all" --no-python-fix-name --python-package-name-prefix "${FED_PYTHON}" --python-bin ${FED_PYTHON} --python-pip ${FED_PIP} --python-install-lib="lib/${FED_PYTHON}/site-packages/" -d ${FED_PYTHON} -d iputils -d "NetworkManager >= 1.2.0" -d openvpn -d NetworkManager-openvpn-gnome -s python -t rpm ${PACKAGE}'
79+
- docker run -e PACKAGE_DIR=${PACKAGE_DIR} -e DEB_PYTHON=$DEB_PYTHON -e DEB_PIP=$DEB_PIP -e PYTHON_MIN_VERSION=$PYTHON_MIN_VERSION -e DESCRIPTION="${DESCRIPTION}" -e PACKAGE=$PACKAGE -e GH_USER=$GH_USER -e NAME=$NAME -v $PWD:/mnt/travis ${DEB_DOCKER} /bin/bash -c 'su && cd /mnt/travis && apt-get update && apt-get -y install ${DEB_PYTHON} ${DEB_PYTHON}-pip ruby ruby-dev rubygems-integration build-essential && gem install --no-ri --no-rdoc fpm && ${DEB_PYTHON} --version && fpm --maintainer "${GH_USER}" --description "${DESCRIPTION}" --prefix "${PACKAGE_DIR}" --url "https://github.com/${GH_USER}/${NAME}" --provides "${PACKAGE}" -a "all" --no-python-fix-name --python-package-name-prefix "${DEB_PYTHON}" --python-bin ${DEB_PYTHON} --python-pip ${DEB_PIP} --python-install-bin "/usr/bin/" --python-install-lib "/lib/${DEB_PYTHON}/dist-packages/" -d "${DEB_PYTHON} >= ${PYTHON_MIN_VERSION}" -d iputils-ping -d "network-manager >= 1.2.0" -d openvpn -d network-manager-openvpn-gnome -s python -t deb ${PACKAGE}'
80+
- docker run -e PACKAGE_DIR=${PACKAGE_DIR} -e FED_PYTHON=$FED_PYTHON -e FED_PIP=$FED_PIP -e DESCRIPTION="${DESCRIPTION}" -e PACKAGE=$PACKAGE -e GH_USER=$GH_USER -e NAME=$NAME -v $PWD:/mnt/travis ${FED_DOCKER} /bin/bash -c 'su && cd /mnt/travis && dnf install ruby-devel gcc make rpm-build libffi-devel ${FED_PYTHON} ${FED_PYTHON}-pip -y && gem install --no-ri --no-rdoc fpm && ${FED_PYTHON} --version && fpm --maintainer "${GH_USER}" --description "${DESCRIPTION}" --prefix "${PACKAGE_DIR}" --url "https://github.com/${GH_USER}/${NAME}" --provides "${PACKAGE}" -a "all" --no-python-fix-name --python-package-name-prefix "${FED_PYTHON}" --python-bin ${FED_PYTHON} --python-pip ${FED_PIP} --python-install-bin "/usr/bin/" --python-install-lib "/lib/${FED_PYTHON}/site-packages/" -d "python3 >= 3.6.0" -d iputils -d "NetworkManager >= 1.2.0" -d openvpn -d NetworkManager-openvpn-gnome -s python -t rpm ${PACKAGE}'
8181

8282
before_deploy:
8383
- pwd && cd ../

nordnm/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__package__ = "nordnm"
2-
__version__ = "0.4.0"
2+
__version__ = "0.5.0"
33
__license__ = "GNU General Public License v3 or later (GPLv3+)"

nordnm/benchmarking.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from functools import partial
77
import numpy
88
import os
9+
import sys
910
import subprocess
1011
from decimal import Decimal
1112
import resource
@@ -20,8 +21,8 @@ def get_server_score(server, ping_attempts):
2021
score = 0 # Lowest starting score
2122
rtt = None
2223

23-
# If a server is at 90% load or greater, we don't need to waste time pinging. Just keep starting score.
24-
if load < 90:
24+
# If a server is at 95% load or greater, we don't need to waste time pinging. Just keep starting score.
25+
if load < 95:
2526
rtt, loss = utils.get_rtt_loss(ip_addr, ping_attempts)
2627

2728
if loss < 5: # Similarly, if packet loss is >= 5%, the connection is not reliable. Keep the starting score.
@@ -37,7 +38,7 @@ def compare_server(server, best_servers, ping_attempts, valid_protocols, valid_c
3738
if server['features']['openvpn_tcp'] and 'tcp' in valid_protocols:
3839
supported_protocols.append('tcp')
3940

40-
country_code = server['flag']
41+
country_code = server['flag'].lower()
4142
domain = server['domain']
4243
score, load, latency = get_server_score(server, ping_attempts)
4344

@@ -51,7 +52,7 @@ def compare_server(server, best_servers, ping_attempts, valid_protocols, valid_c
5152
category_short_name = nordapi.VPN_CATEGORIES[category['name']]
5253

5354
for protocol in supported_protocols:
54-
best_score = 0
55+
best_score = -1
5556

5657
if best_servers.get((country_code, category_short_name, protocol)):
5758
best_score = best_servers[country_code, category_short_name, protocol]['score']
@@ -82,15 +83,26 @@ def get_num_processes(num_servers):
8283
return num_servers
8384

8485

85-
def get_best_servers(server_list, ping_attempts, valid_protocols, valid_categories):
86+
def get_best_servers(server_list, ping_attempts, valid_protocols, valid_categories, slow_mode=False):
8687
manager = multiprocessing.Manager()
8788
best_servers = manager.dict()
8889

8990
num_servers = len(server_list)
90-
num_processes = get_num_processes(num_servers)
91+
92+
if slow_mode:
93+
num_processes = multiprocessing.cpu_count()
94+
else:
95+
num_processes = get_num_processes(num_servers)
9196

9297
pool = multiprocessing.Pool(num_processes, maxtasksperchild=1)
93-
results = pool.map(partial(compare_server, best_servers=best_servers, ping_attempts=ping_attempts, valid_protocols=valid_protocols, valid_categories=valid_categories), server_list)
98+
99+
results = []
100+
for i, result in enumerate(pool.imap(partial(compare_server, best_servers=best_servers, ping_attempts=ping_attempts, valid_protocols=valid_protocols, valid_categories=valid_categories), server_list)):
101+
sys.stderr.write("\r[INFO] %i/%i benchmarks finished." % (i + 1, num_servers))
102+
results.append(result)
103+
104+
sys.stderr.write('\n')
105+
94106
pool.close()
95107

96108
num_success = results.count(True)

nordnm/nordapi.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
'P2P': 'p2p',
1414
'Double VPN': 'double',
1515
'Dedicated IP servers': 'dedicated',
16-
'Onion over VPN': 'onion',
16+
'Onion Over VPN': 'onion',
1717
'Anti DDoS': 'ddos',
1818
}
1919

nordnm/nordnm.py

+17-9
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def __init__(self):
6565
list_parser.set_defaults(list=True)
6666

6767
sync_parser = subparsers.add_parser('sync', aliases=['s'], help="Synchronise the optimal servers (based on load and latency) to NetworkManager.")
68+
sync_parser.add_argument('-s', '--slow-mode', help="Run benchmarking in 'slow mode'. May increase benchmarking success by pinging servers at a slower rate.", action='store_true')
6869
sync_parser.add_argument('-p', '--preserve-vpn', help="When provided, synchronising will preserve any active VPN instead of disabling it for more accurate benchmarking.", action='store_true')
6970
sync_parser.add_argument('-u', '--update-configs', help='Download the latest OpenVPN configurations from NordVPN.', action='store_true', default=False)
7071
sync_parser.add_argument("-k", "--kill-switch", help="Sets a network kill-switch, to disable the active network interface when an active VPN connection disconnects.", action="store_true")
@@ -205,7 +206,7 @@ def __init__(self):
205206

206207
# Now check for commands that can be chained...
207208
if "sync" in args and args.sync:
208-
self.sync(args.update_configs, args.preserve_vpn)
209+
self.sync(args.update_configs, args.preserve_vpn, args.slow_mode)
209210

210211
if args.kill_switch:
211212
networkmanager.set_killswitch()
@@ -369,14 +370,14 @@ def get_configs(self):
369370

370371
return True
371372

372-
def sync(self, update_config=True, preserve_vpn=False):
373+
def sync(self, update_config=True, preserve_vpn=False, slow_mode=False):
373374
if self.remove_legacy_files():
374375
self.logger.info("Removed legacy files")
375376

376377
if update_config:
377378
self.get_configs()
378379

379-
if self.sync_servers(preserve_vpn):
380+
if self.sync_servers(preserve_vpn, slow_mode):
380381
networkmanager.reload_connections()
381382

382383
def remove_data(self):
@@ -414,7 +415,7 @@ def get_ovpn_path(self, domain, protocol):
414415

415416
def enable_auto_connect(self, country_code: str, category: str='normal', protocol: str='tcp'):
416417
enabled = False
417-
selected_parameters = (country_code.upper(), category, protocol)
418+
selected_parameters = (country_code.lower(), category.lower(), protocol.lower())
418419

419420
if selected_parameters in self.active_servers:
420421
connection_name = self.active_servers[selected_parameters]['name']
@@ -505,7 +506,7 @@ def get_valid_servers(self, servers):
505506
valid_server_list = []
506507

507508
for server in servers:
508-
country_code = server['flag']
509+
country_code = server['flag'].lower()
509510

510511
# If the server country has been selected, it has a selected protocol and selected categories
511512
if self.country_is_selected(country_code) and self.has_valid_protocol(server) and self.has_valid_categories(server):
@@ -528,12 +529,16 @@ def configs_exist(self):
528529
else:
529530
return False
530531

531-
def sync_servers(self, preserve_vpn):
532+
def sync_servers(self, preserve_vpn, slow_mode):
532533
updated = False
533534

534535
username = self.credentials.get_username()
535536
password = self.credentials.get_password()
536-
dns_list = nordapi.get_nameservers()
537+
538+
# Check if there are custom DNS servers specified in the settings before loading the defaults
539+
dns_list = self.settings.get_custom_dns_servers()
540+
if not dns_list:
541+
dns_list = nordapi.get_nameservers()
537542

538543
if not self.configs_exist():
539544
self.logger.warning("No OpenVPN configuration files found.")
@@ -567,14 +572,17 @@ def sync_servers(self, preserve_vpn):
567572
else:
568573
self.logger.warning("Active VPN preserved. This may give unreliable results!")
569574

575+
if slow_mode:
576+
self.logger.info("Benchmarking slow mode enabled.")
577+
570578
num_servers = len(valid_server_list)
571579
self.logger.info("Benchmarking %i servers...", num_servers)
572580

573581
start = timer()
574582
ping_attempts = self.settings.get_ping_attempts() # We are going to be multiprocessing within a class instance, so this needs getting outside of the multiprocessing
575583
valid_protocols = self.settings.get_protocols()
576584
valid_categories = self.settings.get_categories()
577-
best_servers, num_success = benchmarking.get_best_servers(valid_server_list, ping_attempts, valid_protocols, valid_categories)
585+
best_servers, num_success = benchmarking.get_best_servers(valid_server_list, ping_attempts, valid_protocols, valid_categories, slow_mode)
578586

579587
end = timer()
580588

@@ -586,7 +594,7 @@ def sync_servers(self, preserve_vpn):
586594
self.logger.info("Benchmarked %i servers successfully (%0.2f%%). Took %0.2f seconds.", num_success, percent_success, end - start)
587595

588596
if percent_success < 90.0:
589-
self.logger.warning("A large quantity of tests failed. You might want to check the reliability of your network.")
597+
self.logger.warning("A large quantity of tests failed. Your network may be unreliable, or blocking large-scale ICMP requests. Syncing in slow mode (-s) may fix this.")
590598

591599
# remove all old connections and any auto-connect, until a better sync routine is added
592600
if self.remove_active_connections():

nordnm/settings.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ def save_new_settings(self):
3434
# Populate the Countries section with out input data
3535
self.settings.add_section('Countries')
3636
self.settings.set('Countries', '# simply write country codes separated by spaces e.g. country-blacklist = GB US')
37-
self.settings.set('Countries', 'country-blacklist', blacklist)
37+
self.settings.set('Countries', 'country-blacklist', blacklist.lower().strip())
3838
self.settings.set('Countries', '\n# same as above. If this is non-empty, the blacklist is ignored')
39-
self.settings.set('Countries', 'country-whitelist', whitelist)
39+
self.settings.set('Countries', 'country-whitelist', whitelist.lower().strip())
4040

4141
# Prompt for which categories to enable
4242
self.settings.add_section('Categories')
@@ -50,6 +50,11 @@ def save_new_settings(self):
5050
answer = str(utils.input_yes_no("Enable UDP configurations?")).lower()
5151
self.settings.set('Protocols', 'udp', answer)
5252

53+
self.settings.add_section('DNS')
54+
print("\nWARNING: Setting custom DNS servers can compromise your privacy if you don't know what you're doing.")
55+
custom_dns = input("Input custom DNS servers you would like to use, separated by spaces. (Press enter to skip): ")
56+
self.settings.set('DNS', 'custom-dns-servers', custom_dns.strip())
57+
5358
self.settings.add_section('Benchmarking')
5459
ping_attempts = input("Input how many ping attempts to make when benchmarking servers (Default: %i attempts): " % self.DEFAULT_PING_ATTEMPTS)
5560
if not ping_attempts:
@@ -82,14 +87,14 @@ def load(self):
8287
def get_blacklist(self):
8388
blacklist = self.settings.get('Countries', 'country-blacklist')
8489
if blacklist:
85-
return [code.upper() for code in blacklist.split(' ')]
90+
return [code.lower() for code in blacklist.split(' ')]
8691
else:
8792
return None
8893

8994
def get_whitelist(self):
9095
whitelist = self.settings.get('Countries', 'country-whitelist')
9196
if whitelist:
92-
return [code.upper() for code in whitelist.split(' ')]
97+
return [code.lower() for code in whitelist.split(' ')]
9398
else:
9499
return None
95100

@@ -125,3 +130,11 @@ def get_ping_attempts(self):
125130
self.logger.warning("Invalid ping-attempts value. Using default value of %d.", self.DEFAULT_PING_ATTEMPTS)
126131
self.settings.set('Benchmarking', 'ping-attempts', str(self.DEFAULT_PING_ATTEMPTS)) # Lets set the default, so we only get this warning once
127132
return self.DEFAULT_PING_ATTEMPTS
133+
134+
def get_custom_dns_servers(self) -> list:
135+
custom_dns = self.settings.get('DNS', 'custom-dns-servers')
136+
137+
if custom_dns:
138+
return custom_dns.split(' ')
139+
else:
140+
return []

0 commit comments

Comments
 (0)