From 98e0c2c76081b8d724991e837b8ff21fbee14f2f Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 24 Jul 2019 12:24:49 -0400 Subject: [PATCH 1/4] Use random ports for Tor ORPort and obfs4 port Addresses the issue raised in #101 to try and mitigate Tor bridges being easily discoverable by Internet-wide scans (Tor bridges usually use port 9001, Streisand installs have been using 9443/8443 for a while and have somewhat of a "fingerprint") --- playbooks/roles/tor-bridge/defaults/main.yml | 32 ++++++++++++++++++-- playbooks/roles/tor-bridge/tasks/main.yml | 9 ++++++ playbooks/roles/tor-bridge/vars/main.yml | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/playbooks/roles/tor-bridge/defaults/main.yml b/playbooks/roles/tor-bridge/defaults/main.yml index 03e1df306..1396b88eb 100644 --- a/playbooks/roles/tor-bridge/defaults/main.yml +++ b/playbooks/roles/tor-bridge/defaults/main.yml @@ -1,6 +1,34 @@ --- -tor_orport: 8443 -tor_obfs4_port: 9443 +tor_internal_hidden_service_port: 8181 +# Randomize Tor's ORPort and obfs4 port to avoid being +# easily fingerprinted as a Tor bridge (or a Streisand Tor bridge) +# See https://lists.torproject.org/pipermail/tor-dev/2014-December/007957.html +# and https://github.com/StreisandEffect/streisand/issues/101 +# for discussion/more data on the topic. +# Keep track of ports already being used by other services to avoid port conflicts +# "map" call is needed here because some of these vars are strings, others are integers +tor_unavailable_ports: "{{ [ + nginx_port, + ssh_port, + le_port, + shadowsocks_server_port, + wireguard_port, + ocserv_port, + openvpn_port, + stunnel_remote_port, + cloudflared_port, + tor_internal_hidden_service_port +] | map('int') | list }}" +# Choose a random port between 1024-32768--below 1024 would require elevated privileges, +# and any port above 32768 will be used as "Ephemeral Ports" by anything listening locally +# (see output of "cat /proc/sys/net/ipv4/ip_local_port_range" for the full range) +tor_random_port_range: "{{ range(1024, 32768) | list }}" +# Avoid the common ORPort and obfs4 ports +tor_common_ports: [9001, 8443, 9443] + +# Calculate available ports by excluding common/used ports from our choices for randomization +tor_available_ports: "{{ tor_random_port_range | difference(tor_common_ports + tor_unavailable_ports) }}" + # By default Streisand does *not* publish the Tor relay's service descriptor to # the tor network. Using a value of 0 ensures the relay is private to the diff --git a/playbooks/roles/tor-bridge/tasks/main.yml b/playbooks/roles/tor-bridge/tasks/main.yml index 75ef49b80..b6e7acf03 100644 --- a/playbooks/roles/tor-bridge/tasks/main.yml +++ b/playbooks/roles/tor-bridge/tasks/main.yml @@ -22,6 +22,15 @@ - obfs4proxy - tor +- name: "Randomize Tor's ORPort" + set_fact: + tor_orport: "{{ tor_available_ports | random }}" + +- name: "Randomize Tor's obfs4 port" + set_fact: + # The "reject" statement is to avoid using the same random port just chosen for tor_orport + tor_obfs4_port: "{{ tor_available_ports | reject('equalto', 'tor_orport') | list | random }}" + # Update the firewall to allow Tor and obfs4proxy # NOTE(@cpu): we do this early in the role because the Tor daemon will check if # the obfs4proxy port is externally accessible during startup and we want to diff --git a/playbooks/roles/tor-bridge/vars/main.yml b/playbooks/roles/tor-bridge/vars/main.yml index 9ea1e3dbf..1892392ae 100644 --- a/playbooks/roles/tor-bridge/vars/main.yml +++ b/playbooks/roles/tor-bridge/vars/main.yml @@ -18,4 +18,4 @@ tor_html_instructions: "{{ tor_gateway_location }}/index.html" tor_obfs4_qr_code: "{{ tor_gateway_location }}/tor-obfs4-qr-code.png" -tor_internal_hidden_service_address: "127.0.0.1:8181" +tor_internal_hidden_service_address: "127.0.0.1:{{ tor_internal_hidden_service_port }}" From 16d8bd41a271e084646453de6d54b33a37962944 Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 24 Jul 2019 14:01:58 -0400 Subject: [PATCH 2/4] Fix Tor port variables being unusable outside of Tor role --- playbooks/roles/tor-bridge/defaults/main.yml | 9 +++++++++ playbooks/roles/tor-bridge/tasks/main.yml | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/playbooks/roles/tor-bridge/defaults/main.yml b/playbooks/roles/tor-bridge/defaults/main.yml index 1396b88eb..88ed370e6 100644 --- a/playbooks/roles/tor-bridge/defaults/main.yml +++ b/playbooks/roles/tor-bridge/defaults/main.yml @@ -29,6 +29,15 @@ tor_common_ports: [9001, 8443, 9443] # Calculate available ports by excluding common/used ports from our choices for randomization tor_available_ports: "{{ tor_random_port_range | difference(tor_common_ports + tor_unavailable_ports) }}" +# Pick a random port from list of available ports +# N.B.: random with "seed" parameter is needed here to enable a random-but-idempotent +# number, otherwise this number will be different every time the variable is referenced +# in a separate playbook/task (e.g. an EC2 firewall task) +# For the seed we use the hostname of the target VM along with a small identifier for the variable +# this is to avoid getting the same value for both vars, while still maintaining a unique seed. +tor_orport: "{{ tor_available_ports | random(seed=inventory_hostname+'_tor_orport') }}" +# The "reject" statement is to avoid using the same random port just chosen for tor_orport +tor_obfs4_port: "{{ tor_available_ports | reject('equalto', 'tor_orport') | list | random(seed=inventory_hostname+'_tor_obfs4_port') }}" # By default Streisand does *not* publish the Tor relay's service descriptor to # the tor network. Using a value of 0 ensures the relay is private to the diff --git a/playbooks/roles/tor-bridge/tasks/main.yml b/playbooks/roles/tor-bridge/tasks/main.yml index b6e7acf03..75ef49b80 100644 --- a/playbooks/roles/tor-bridge/tasks/main.yml +++ b/playbooks/roles/tor-bridge/tasks/main.yml @@ -22,15 +22,6 @@ - obfs4proxy - tor -- name: "Randomize Tor's ORPort" - set_fact: - tor_orport: "{{ tor_available_ports | random }}" - -- name: "Randomize Tor's obfs4 port" - set_fact: - # The "reject" statement is to avoid using the same random port just chosen for tor_orport - tor_obfs4_port: "{{ tor_available_ports | reject('equalto', 'tor_orport') | list | random }}" - # Update the firewall to allow Tor and obfs4proxy # NOTE(@cpu): we do this early in the role because the Tor daemon will check if # the obfs4proxy port is externally accessible during startup and we want to From 5705da30348f95a82598d73bd00f18d218b22c3a Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 24 Jul 2019 14:44:10 -0400 Subject: [PATCH 3/4] Fix undefined variable cloudflared_port when setting up cloud firewalls --- playbooks/amazon.yml | 3 +++ playbooks/azure.yml | 3 +++ playbooks/google.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/playbooks/amazon.yml b/playbooks/amazon.yml index 72eb6df3d..882166283 100644 --- a/playbooks/amazon.yml +++ b/playbooks/amazon.yml @@ -37,6 +37,9 @@ - roles/lets-encrypt/vars/main.yml - roles/streisand-gateway/defaults/main.yml - roles/stunnel/defaults/main.yml + # Cloudflared is included to ensure the Tor variable file is able + # to reference other ports when defining its own variables. + - roles/cloudflared/defaults/main.yml - roles/tor-bridge/defaults/main.yml - roles/wireguard/defaults/main.yml diff --git a/playbooks/azure.yml b/playbooks/azure.yml index e34115d4b..c7a614f7a 100644 --- a/playbooks/azure.yml +++ b/playbooks/azure.yml @@ -56,6 +56,9 @@ - roles/lets-encrypt/vars/main.yml - roles/streisand-gateway/defaults/main.yml - roles/stunnel/defaults/main.yml + # Cloudflared is included to ensure the Tor variable file is able + # to reference other ports when defining its own variables. + - roles/cloudflared/defaults/main.yml - roles/tor-bridge/defaults/main.yml - roles/wireguard/defaults/main.yml diff --git a/playbooks/google.yml b/playbooks/google.yml index 8996f583c..2a42fe30b 100644 --- a/playbooks/google.yml +++ b/playbooks/google.yml @@ -60,6 +60,9 @@ - roles/lets-encrypt/vars/main.yml - roles/streisand-gateway/defaults/main.yml - roles/stunnel/defaults/main.yml + # Cloudflared is included to ensure the Tor variable file is able + # to reference other ports when defining its own variables. + - roles/cloudflared/defaults/main.yml - roles/tor-bridge/defaults/main.yml - roles/wireguard/defaults/main.yml From 4300ca2ea950f45233df7a8be4fc01c9f8b9701d Mon Sep 17 00:00:00 2001 From: Nick Gnazzo Date: Fri, 26 Jul 2019 19:52:28 -0400 Subject: [PATCH 4/4] Fix issues of Tor randomizing idempotence and variable access across playbooks --- playbooks/amazon.yml | 7 +- playbooks/azure.yml | 7 +- playbooks/digitalocean.yml | 3 + playbooks/existing-server.yml | 3 + playbooks/google.yml | 7 +- playbooks/linode.yml | 3 + playbooks/localhost.yml | 4 + playbooks/rackspace.yml | 3 + playbooks/roles/tor-bridge/defaults/main.yml | 41 +-------- playbooks/tor-setup.yml | 88 ++++++++++++++++++++ 10 files changed, 118 insertions(+), 48 deletions(-) create mode 100644 playbooks/tor-setup.yml diff --git a/playbooks/amazon.yml b/playbooks/amazon.yml index 882166283..d4ff66dea 100644 --- a/playbooks/amazon.yml +++ b/playbooks/amazon.yml @@ -1,4 +1,8 @@ --- +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +# We do this early so ec2-security-group knows Tor's port +- import_playbook: tor-setup.yml + - name: Provision the EC2 Server # ============================== hosts: localhost @@ -37,9 +41,6 @@ - roles/lets-encrypt/vars/main.yml - roles/streisand-gateway/defaults/main.yml - roles/stunnel/defaults/main.yml - # Cloudflared is included to ensure the Tor variable file is able - # to reference other ports when defining its own variables. - - roles/cloudflared/defaults/main.yml - roles/tor-bridge/defaults/main.yml - roles/wireguard/defaults/main.yml diff --git a/playbooks/azure.yml b/playbooks/azure.yml index c7a614f7a..6eabbd36d 100644 --- a/playbooks/azure.yml +++ b/playbooks/azure.yml @@ -1,4 +1,8 @@ --- +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +# We do this early so azure-security-group knows Tor's port +- import_playbook: tor-setup.yml + - name: Provision the Azure Server (Resource Manager mode) # ======================================================== hosts: localhost @@ -56,9 +60,6 @@ - roles/lets-encrypt/vars/main.yml - roles/streisand-gateway/defaults/main.yml - roles/stunnel/defaults/main.yml - # Cloudflared is included to ensure the Tor variable file is able - # to reference other ports when defining its own variables. - - roles/cloudflared/defaults/main.yml - roles/tor-bridge/defaults/main.yml - roles/wireguard/defaults/main.yml diff --git a/playbooks/digitalocean.yml b/playbooks/digitalocean.yml index 3ba5fd9e2..ddcda8780 100644 --- a/playbooks/digitalocean.yml +++ b/playbooks/digitalocean.yml @@ -1,4 +1,7 @@ --- +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +- import_playbook: tor-setup.yml + - name: Provision the DigitalOcean Server # ======================================= hosts: localhost diff --git a/playbooks/existing-server.yml b/playbooks/existing-server.yml index 2430f4bc9..0188ef955 100644 --- a/playbooks/existing-server.yml +++ b/playbooks/existing-server.yml @@ -3,6 +3,9 @@ # role to create a new server and instead applies Streisand to an existing # remote server. +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +- import_playbook: tor-setup.yml + - name: Register the genesis role in use hosts: localhost gather_facts: yes diff --git a/playbooks/google.yml b/playbooks/google.yml index 2a42fe30b..6524ece31 100644 --- a/playbooks/google.yml +++ b/playbooks/google.yml @@ -1,4 +1,8 @@ --- +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +# We do this early so gce-network knows Tor's port +- import_playbook: tor-setup.yml + - name: Provision the GCE Server # ======================================= hosts: localhost @@ -60,9 +64,6 @@ - roles/lets-encrypt/vars/main.yml - roles/streisand-gateway/defaults/main.yml - roles/stunnel/defaults/main.yml - # Cloudflared is included to ensure the Tor variable file is able - # to reference other ports when defining its own variables. - - roles/cloudflared/defaults/main.yml - roles/tor-bridge/defaults/main.yml - roles/wireguard/defaults/main.yml diff --git a/playbooks/linode.yml b/playbooks/linode.yml index 5cca1bca2..362d20913 100644 --- a/playbooks/linode.yml +++ b/playbooks/linode.yml @@ -1,4 +1,7 @@ --- +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +- import_playbook: tor-setup.yml + - name: Provision the Linode Server # ================================= hosts: localhost diff --git a/playbooks/localhost.yml b/playbooks/localhost.yml index 010dab608..205a16fcd 100644 --- a/playbooks/localhost.yml +++ b/playbooks/localhost.yml @@ -2,6 +2,10 @@ # localhost.yml is an advanced provisioning option that doesn't use a genesis # role to create a new server and instead applies Streisand to the localhost. +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +# We do this early so gce-network knows Tor's port +- import_playbook: tor-setup.yml + # Ensure Python is installed on the system - import_playbook: python.yml diff --git a/playbooks/rackspace.yml b/playbooks/rackspace.yml index 8e6d05a68..890769a8d 100644 --- a/playbooks/rackspace.yml +++ b/playbooks/rackspace.yml @@ -1,4 +1,7 @@ --- +# Randomize Tor's ORPort and obfs4 port if Tor is enabled. +- import_playbook: tor-setup.yml + - name: Provision the Rackspace Server # ==================================== hosts: localhost diff --git a/playbooks/roles/tor-bridge/defaults/main.yml b/playbooks/roles/tor-bridge/defaults/main.yml index 88ed370e6..d654a26bc 100644 --- a/playbooks/roles/tor-bridge/defaults/main.yml +++ b/playbooks/roles/tor-bridge/defaults/main.yml @@ -1,44 +1,7 @@ --- +tor_orport: "{{ lookup('file', lookup('env','HOME') + '/.streisand/tor-orport') or 0 }}" +tor_obfs4_port: "{{ lookup('file', lookup('env','HOME') + '/.streisand/tor-obfs4-port') or 0 }}" tor_internal_hidden_service_port: 8181 -# Randomize Tor's ORPort and obfs4 port to avoid being -# easily fingerprinted as a Tor bridge (or a Streisand Tor bridge) -# See https://lists.torproject.org/pipermail/tor-dev/2014-December/007957.html -# and https://github.com/StreisandEffect/streisand/issues/101 -# for discussion/more data on the topic. -# Keep track of ports already being used by other services to avoid port conflicts -# "map" call is needed here because some of these vars are strings, others are integers -tor_unavailable_ports: "{{ [ - nginx_port, - ssh_port, - le_port, - shadowsocks_server_port, - wireguard_port, - ocserv_port, - openvpn_port, - stunnel_remote_port, - cloudflared_port, - tor_internal_hidden_service_port -] | map('int') | list }}" -# Choose a random port between 1024-32768--below 1024 would require elevated privileges, -# and any port above 32768 will be used as "Ephemeral Ports" by anything listening locally -# (see output of "cat /proc/sys/net/ipv4/ip_local_port_range" for the full range) -tor_random_port_range: "{{ range(1024, 32768) | list }}" -# Avoid the common ORPort and obfs4 ports -tor_common_ports: [9001, 8443, 9443] - -# Calculate available ports by excluding common/used ports from our choices for randomization -tor_available_ports: "{{ tor_random_port_range | difference(tor_common_ports + tor_unavailable_ports) }}" - -# Pick a random port from list of available ports -# N.B.: random with "seed" parameter is needed here to enable a random-but-idempotent -# number, otherwise this number will be different every time the variable is referenced -# in a separate playbook/task (e.g. an EC2 firewall task) -# For the seed we use the hostname of the target VM along with a small identifier for the variable -# this is to avoid getting the same value for both vars, while still maintaining a unique seed. -tor_orport: "{{ tor_available_ports | random(seed=inventory_hostname+'_tor_orport') }}" -# The "reject" statement is to avoid using the same random port just chosen for tor_orport -tor_obfs4_port: "{{ tor_available_ports | reject('equalto', 'tor_orport') | list | random(seed=inventory_hostname+'_tor_obfs4_port') }}" - # By default Streisand does *not* publish the Tor relay's service descriptor to # the tor network. Using a value of 0 ensures the relay is private to the # streisand operator and their users. See the Tor documentation[0] for more diff --git a/playbooks/tor-setup.yml b/playbooks/tor-setup.yml new file mode 100644 index 000000000..b20b0f20d --- /dev/null +++ b/playbooks/tor-setup.yml @@ -0,0 +1,88 @@ +--- +# We randomize the Tor ORPort and obfs4 port fairly early +# in the provisioning process to allow cloud firewalls/playbooks +# to access the tor_orport and tor_obfs4_port variables +# which are chosen randomly. Since we use ansible's "random" filter, +# we run into issues of idempotence if we don't use set_fact to +# make the call to "random". Even if we provide a seed, we +# only get random-but-idempotent behavior within the ansible role. +# Meaning the genesis- roles would have a different number +# for tor_orport and tor_obfs4_port than the number generated when +# the tor-bridge role runs, even if we provide a fixed seed. + +# We randomize Tor's ORPort and obfs4 port to avoid being +# easily fingerprinted as a Tor bridge (or a Streisand Tor bridge) +# See https://lists.torproject.org/pipermail/tor-dev/2014-December/007957.html +# and https://github.com/StreisandEffect/streisand/issues/101 +# for discussion/more data on the topic. +- name: Randomize Tor's ORPort and obfs4 port + hosts: localhost + gather_facts: no + tasks: + - lineinfile: + path: "{{ lookup('env', 'HOME') }}/.streisand/tor-orport" + regexp: "^(.*)$" + state: absent + + - lineinfile: + path: "{{ lookup('env', 'HOME') }}/.streisand/tor-obfs4-port" + regexp: "^(.*)$" + state: absent + + - name: Randomize Tor's ORPort + run_once: true + lineinfile: + path: "{{ lookup('env', 'HOME') }}/.streisand/tor-orport" + regexp: "^[0-9]+$" + create: yes + # Pick a random port from list of available ports + line: "{{ tor_available_ports | random }}" + + - name: Randomize Tor's obfs4 port + run_once: true + lineinfile: + path: "{{ lookup('env', 'HOME') }}/.streisand/tor-obfs4-port" + regexp: "^[0-9]+$" + create: yes + # The "reject" statement is to avoid using the same random port just chosen for tor_orport + line: "{{ tor_available_ports | reject('equalto', 'tor_orport') | list | random }}" + + + vars: + # Keep track of ports already being used by other services to avoid port conflicts + # "map" call is needed here because some of these vars are strings, others are integers + tor_unavailable_ports: "{{ [ + nginx_port, + ssh_port, + le_port, + shadowsocks_server_port, + wireguard_port, + ocserv_port, + openvpn_port, + stunnel_remote_port, + cloudflared_port, + tor_internal_hidden_service_port + ] | map('int') | list }}" + # Choose a random port between 1024-32768--below 1024 would require elevated privileges, + # and any port above 32768 will be used as "Ephemeral Ports" by anything listening locally + # (see output of "cat /proc/sys/net/ipv4/ip_local_port_range" for the full range) + tor_random_port_range: "{{ range(1024, 32768) | list }}" + # Avoid the common ORPort and obfs4 ports + tor_common_ports: [9001, 8443, 9443] + # Calculate available ports by excluding common/used ports from our choices for randomization + tor_available_ports: "{{ tor_random_port_range | difference(tor_common_ports + tor_unavailable_ports) }}" + + # These variable files are included so we can have a list of all + # used ports, this way we can avoid port conflicts when randomizing + # Tor's ORPort and obfs4 port. + vars_files: + - roles/openconnect/defaults/main.yml + - roles/openvpn/defaults/main.yml + - roles/shadowsocks/defaults/main.yml + - roles/ssh/defaults/main.yml + - roles/lets-encrypt/vars/main.yml + - roles/streisand-gateway/defaults/main.yml + - roles/stunnel/defaults/main.yml + - roles/cloudflared/defaults/main.yml + - roles/tor-bridge/defaults/main.yml + - roles/wireguard/defaults/main.yml