Refactor to CensorshipCircumvention and Meek classes. Use Meek domain fronting when requesting bridges in frontend

This commit is contained in:
Miguel Jacq 2021-10-19 11:36:03 +11:00
parent bcf697574e
commit 5b4d77c363
No known key found for this signature in database
GPG Key ID: EEA4341C6D97A0B6
6 changed files with 202 additions and 79 deletions

View File

@ -28,6 +28,7 @@ from datetime import timedelta
from .common import Common, CannotFindTor
from .censorship import CensorshipCircumvention
from .meek import Meek, MeekNotRunning
from .web import Web
from .onion import TorErrorProtocolError, TorTooOldEphemeral, TorTooOldStealth, Onion
from .onionshare import OnionShare
@ -284,6 +285,18 @@ def main(cwd=None):
# Create the Web object
web = Web(common, False, mode_settings, mode)
# Create the Meek object and start the meek client
meek = Meek(common)
meek.start()
# Create the CensorshipCircumvention object to make
# API calls to Tor over Meek
censorship = CensorshipCircumvention(common, meek)
# Example: request recommended bridges, pretending to be from China, using
# domain fronting.
# censorship_recommended_settings = censorship.request_settings(country="cn")
# print(censorship_recommended_settings)
# Start the Onion object
try:
onion = Onion(common, use_tmp_dir=True)

View File

@ -18,77 +18,30 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import requests
import subprocess
from .meek import MeekNotRunning
class CensorshipCircumvention:
class CensorshipCircumvention(object):
"""
The CensorShipCircumvention object contains methods to detect
and offer solutions to censorship when connecting to Tor.
Connect to the Tor Moat APIs to retrieve censorship
circumvention recommendations, over the Meek client.
"""
def __init__(self, common):
def __init__(self, common, meek, domain_fronting=True):
"""
Set up the CensorshipCircumvention object to hold
common and meek objects.
"""
self.common = common
self.meek = meek
self.common.log("CensorshipCircumvention", "__init__")
get_tor_paths = self.common.get_tor_paths
(
self.tor_path,
self.tor_geo_ip_file_path,
self.tor_geo_ipv6_file_path,
self.obfs4proxy_file_path,
self.meek_client_file_path,
) = get_tor_paths()
# Bail out if we requested domain fronting but we can't use meek
if domain_fronting and not self.meek.meek_proxies:
raise MeekNotRunning()
meek_url = "https://moat.torproject.org.global.prod.fastly.net/"
meek_front = "cdn.sstatic.net"
meek_env = {
"TOR_PT_MANAGED_TRANSPORT_VER": "1",
"TOR_PT_CLIENT_TRANSPORTS": "meek",
}
# @TODO detect the port from the subprocess output
meek_address = "127.0.0.1"
meek_port = "43533" # hardcoded for testing
self.meek_proxies = {
"http": f"socks5h://{meek_address}:{meek_port}",
"https": f"socks5h://{meek_address}:{meek_port}",
}
# Start the Meek Client as a subprocess.
# This will be used to do domain fronting to the Tor
# Moat API endpoints for censorship circumvention as
# well as BridgeDB lookups.
if self.common.platform == "Windows":
# In Windows, hide console window when opening tor.exe subprocess
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
self.meek_proc = subprocess.Popen(
[self.meek_client_file_path, "--url", meek_url, "--front", meek_front],
stdout=subprocess.PIPE,
startupinfo=startupinfo,
bufsize=1,
env=meek_env,
text=True,
)
else:
self.meek_proc = subprocess.Popen(
[self.meek_client_file_path, "--url", meek_url, "--front", meek_front],
stdout=subprocess.PIPE,
bufsize=1,
env=meek_env,
text=True,
)
# if "CMETHOD meek socks5" in line:
# self.meek_host = (line.split(" ")[3].split(":")[0])
# self.meek_port = (line.split(" ")[3].split(":")[1])
# self.common.log("CensorshipCircumvention", "__init__", f"Meek host is {self.meek_host}")
# self.common.log("CensorshipCircumvention", "__init__", f"Meek port is {self.meek_port}")
def censorship_obtain_map(self, country=False):
def request_map(self, country=False):
"""
Retrieves the Circumvention map from Tor Project and store it
locally for further look-ups if required.
@ -108,7 +61,7 @@ class CensorshipCircumvention:
endpoint,
json=data,
headers={"Content-Type": "application/vnd.api+json"},
proxies=self.meek_proxies,
proxies=self.meek.meek_proxies,
)
if r.status_code != 200:
self.common.log(
@ -130,7 +83,7 @@ class CensorshipCircumvention:
return result
def censorship_obtain_settings(self, country=False, transports=False):
def request_settings(self, country=False, transports=False):
"""
Retrieves the Circumvention Settings from Tor Project, which
will return recommended settings based on the country code of
@ -152,7 +105,7 @@ class CensorshipCircumvention:
endpoint,
json=data,
headers={"Content-Type": "application/vnd.api+json"},
proxies=self.meek_proxies,
proxies=self.meek.meek_proxies,
)
if r.status_code != 200:
self.common.log(
@ -185,7 +138,7 @@ class CensorshipCircumvention:
return result
def censorship_obtain_builtin_bridges(self):
def request_builtin_bridges(self):
"""
Retrieves the list of built-in bridges from the Tor Project.
"""
@ -193,7 +146,7 @@ class CensorshipCircumvention:
r = requests.post(
endpoint,
headers={"Content-Type": "application/vnd.api+json"},
proxies=self.meek_proxies,
proxies=self.meek.meek_proxies,
)
if r.status_code != 200:
self.common.log(

144
cli/onionshare_cli/meek.py Normal file
View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2021 Micah Lee, et al. <micah@micahflee.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import subprocess
from queue import Queue, Empty
from threading import Thread
class Meek(object):
"""
The Meek object starts the meek-client as a subprocess.
This process is used to do domain-fronting to connect to
the Tor APIs for censorship circumvention and retrieving
bridges, before connecting to Tor.
"""
def __init__(self, common):
"""
Set up the Meek object
"""
self.common = common
self.common.log("Meek", "__init__")
get_tor_paths = self.common.get_tor_paths
(
self.tor_path,
self.tor_geo_ip_file_path,
self.tor_geo_ipv6_file_path,
self.obfs4proxy_file_path,
self.snowflake_file_path,
self.meek_client_file_path,
) = get_tor_paths()
self.meek_proxies = {}
self.meek_url = "https://moat.torproject.org.global.prod.fastly.net/"
self.meek_front = "cdn.sstatic.net"
self.meek_env = {
"TOR_PT_MANAGED_TRANSPORT_VER": "1",
"TOR_PT_CLIENT_TRANSPORTS": "meek",
}
self.meek_host = "127.0.0.1"
self.meek_port = None
def start(self):
"""
Start the Meek Client and populate the SOCKS proxies dict
for use with requests to the Tor Moat API.
"""
# Small method to read stdout from the subprocess.
# We use this to obtain the random port that Meek
# started on
def enqueue_output(out, queue):
for line in iter(out.readline, b""):
queue.put(line)
out.close()
# Start the Meek Client as a subprocess.
if self.common.platform == "Windows":
# In Windows, hide console window when opening tor.exe subprocess
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
self.meek_proc = subprocess.Popen(
[
self.meek_client_file_path,
"--url",
self.meek_url,
"--front",
self.meek_front,
],
stdout=subprocess.PIPE,
startupinfo=startupinfo,
bufsize=1,
env=self.meek_env,
text=True,
)
else:
self.meek_proc = subprocess.Popen(
[
self.meek_client_file_path,
"--url",
self.meek_url,
"--front",
self.meek_front,
],
stdout=subprocess.PIPE,
bufsize=1,
env=self.meek_env,
text=True,
)
# Queue up the stdout from the subprocess for polling later
q = Queue()
t = Thread(target=enqueue_output, args=(self.meek_proc.stdout, q))
t.daemon = True # thread dies with the program
t.start()
while True:
# read stdout without blocking
try:
line = q.get_nowait()
except Empty:
# no stdout yet?
pass
else: # we got stdout
if "CMETHOD meek socks5" in line:
self.meek_host = line.split(" ")[3].split(":")[0]
self.meek_port = line.split(" ")[3].split(":")[1]
self.common.log("Meek", "start", f"Meek host is {self.meek_host}")
self.common.log("Meek", "start", f"Meek port is {self.meek_port}")
break
if self.meek_port:
self.meek_proxies = {
"http": f"socks5h://{self.meek_host}:{self.meek_port}",
"https": f"socks5h://{self.meek_host}:{self.meek_port}",
}
else:
self.common.log("Meek", "start", "Could not obtain the meek port")
raise MeekNotRunning()
class MeekNotRunning(Exception):
"""
We were unable to start Meek or obtain the port
number it started on, in order to do domain fronting.
"""

View File

@ -409,11 +409,13 @@ class GuiCommon:
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
snowflake_file_path = os.path.join(base_path, "snowflake-client")
meek_client_file_path = os.path.join(base_path, "meek-client")
else:
# Fallback to looking in the path
tor_path = shutil.which("tor")
obfs4proxy_file_path = shutil.which("obfs4proxy")
snowflake_file_path = shutil.which("snowflake-client")
meek_client_file_path = shutil.which("meek-client")
prefix = os.path.dirname(os.path.dirname(tor_path))
tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
@ -423,6 +425,7 @@ class GuiCommon:
tor_path = os.path.join(base_path, "Tor", "tor.exe")
obfs4proxy_file_path = os.path.join(base_path, "Tor", "obfs4proxy.exe")
snowflake_file_path = os.path.join(base_path, "Tor", "snowflake-client.exe")
meek_client_file_path = os.path.join(base_path, "Tor", "meek-client.exe")
tor_geo_ip_file_path = os.path.join(base_path, "Data", "Tor", "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "Data", "Tor", "geoip6")
elif self.common.platform == "Darwin":
@ -430,6 +433,7 @@ class GuiCommon:
tor_path = os.path.join(base_path, "tor")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
snowflake_file_path = os.path.join(base_path, "snowflake-client")
meek_client_file_path = os.path.join(base_path, "meek-client")
tor_geo_ip_file_path = os.path.join(base_path, "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
elif self.common.platform == "BSD":
@ -437,6 +441,7 @@ class GuiCommon:
tor_geo_ip_file_path = "/usr/local/share/tor/geoip"
tor_geo_ipv6_file_path = "/usr/local/share/tor/geoip6"
obfs4proxy_file_path = "/usr/local/bin/obfs4proxy"
meek_client_file_path = "/usr/local/bin/meek-client"
snowflake_file_path = "/usr/local/bin/snowflake-client"
return (
@ -445,6 +450,7 @@ class GuiCommon:
tor_geo_ipv6_file_path,
obfs4proxy_file_path,
snowflake_file_path,
meek_client_file_path,
)
@staticmethod

View File

@ -34,13 +34,15 @@ class MoatDialog(QtWidgets.QDialog):
got_bridges = QtCore.Signal(str)
def __init__(self, common):
def __init__(self, common, meek):
super(MoatDialog, self).__init__()
self.common = common
self.common.log("MoatDialog", "__init__")
self.meek = meek
self.setModal(True)
self.setWindowTitle(strings._("gui_settings_bridge_moat_button"))
self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
@ -108,7 +110,7 @@ class MoatDialog(QtWidgets.QDialog):
self.submit_button.hide()
# BridgeDB fetch
self.t_fetch = MoatThread(self.common, "fetch")
self.t_fetch = MoatThread(self.common, self.meek, "fetch")
self.t_fetch.bridgedb_error.connect(self.bridgedb_error)
self.t_fetch.captcha_ready.connect(self.captcha_ready)
self.t_fetch.start()
@ -130,6 +132,7 @@ class MoatDialog(QtWidgets.QDialog):
# BridgeDB check
self.t_check = MoatThread(
self.common,
self.meek,
"check",
{"challenge": self.challenge, "solution": self.solution_lineedit.text()},
)
@ -209,17 +212,20 @@ class MoatThread(QtCore.QThread):
captcha_ready = QtCore.Signal(str, str)
bridges_ready = QtCore.Signal(str)
def __init__(self, common, action, data={}):
def __init__(self, common, meek, action, data={}):
super(MoatThread, self).__init__()
self.common = common
self.common.log("MoatThread", "__init__", f"action={action}")
self.meek = meek
self.transport = "obfs4"
self.action = action
self.data = data
def run(self):
# TODO: Do all of this using domain fronting
# Start Meek so that we can do domain fronting
self.meek.start()
if self.action == "fetch":
self.common.log("MoatThread", "run", f"starting fetch")
@ -228,6 +234,7 @@ class MoatThread(QtCore.QThread):
r = requests.post(
"https://bridges.torproject.org/moat/fetch",
headers={"Content-Type": "application/vnd.api+json"},
proxies=self.meek.meek_proxies,
json={
"data": [
{
@ -280,6 +287,7 @@ class MoatThread(QtCore.QThread):
r = requests.post(
"https://bridges.torproject.org/moat/check",
headers={"Content-Type": "application/vnd.api+json"},
proxies=self.meek.meek_proxies,
json={
"data": [
{

View File

@ -24,6 +24,7 @@ import platform
import re
import os
from onionshare_cli.meek import Meek
from onionshare_cli.settings import Settings
from onionshare_cli.onion import Onion
@ -48,6 +49,8 @@ class TorSettingsDialog(QtWidgets.QDialog):
self.common.log("TorSettingsDialog", "__init__")
self.meek = Meek(common)
self.setModal(True)
self.setWindowTitle(strings._("gui_tor_settings_window_title"))
self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
@ -78,6 +81,7 @@ class TorSettingsDialog(QtWidgets.QDialog):
self.tor_geo_ipv6_file_path,
self.obfs4proxy_file_path,
self.snowflake_file_path,
self.meek_client_file_path,
) = self.common.gui.get_tor_paths()
bridges_label = QtWidgets.QLabel(strings._("gui_settings_tor_bridges_label"))
@ -497,7 +501,7 @@ class TorSettingsDialog(QtWidgets.QDialog):
"""
self.common.log("TorSettingsDialog", "bridge_moat_button_clicked")
moat_dialog = MoatDialog(self.common)
moat_dialog = MoatDialog(self.common, self.meek)
moat_dialog.got_bridges.connect(self.bridge_moat_got_bridges)
moat_dialog.exec_()
@ -577,9 +581,7 @@ class TorSettingsDialog(QtWidgets.QDialog):
return
onion = Onion(
self.common,
use_tmp_dir=True,
get_tor_paths=self.common.gui.get_tor_paths,
self.common, use_tmp_dir=True, get_tor_paths=self.common.gui.get_tor_paths
)
tor_con = TorConnectionDialog(self.common, settings, True, onion)
@ -781,10 +783,7 @@ class TorSettingsDialog(QtWidgets.QDialog):
Alert(self.common, strings._("gui_settings_moat_bridges_invalid"))
return False
settings.set(
"tor_bridges_use_moat_bridges",
moat_bridges,
)
settings.set("tor_bridges_use_moat_bridges", moat_bridges)
settings.set("tor_bridges_use_custom_bridges", "")