Merge branch 'master' into master

This commit is contained in:
Jason Evans 2023-06-21 18:11:31 +02:00 committed by GitHub
commit c31c409a1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 3342 additions and 1058 deletions

View file

@ -2,3 +2,5 @@ keys/
*.egg-info *.egg-info
.tox/ .tox/
__cache__ __cache__
__pycache__
*/__pycache__

1
.gitignore vendored
View file

@ -105,3 +105,4 @@ ENV/
# more # more
key/ key/
.vscode

View file

@ -1,5 +1,6 @@
repos:
- repo: git://github.com/pre-commit/pre-commit-hooks - repo: git://github.com/pre-commit/pre-commit-hooks
sha: v0.9.1 rev: v2.5.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-docstring-first - id: check-docstring-first
@ -10,12 +11,14 @@
args: args:
- --exclude=__init__.py - --exclude=__init__.py
language_version: python3 language_version: python3
- id: autopep8-wrapper
language_version: python3
- id: requirements-txt-fixer - id: requirements-txt-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.5.2
hooks:
- id: autopep8
- repo: git://github.com/asottile/reorder_python_imports - repo: git://github.com/asottile/reorder_python_imports
sha: v0.3.5 rev: v2.3.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
language_version: python3 language_version: python3

View file

@ -1,10 +1,9 @@
sudo: false sudo: false
dist: xenial
language: python language: python
python: python:
- "3.4" - '3.8'
- "3.5" install: pip install tox-travis pre-commit poetry
- "3.6"
install: pip install tox-travis pre-commit
script: script:
- pre-commit run --all-files - pre-commit run --all-files
- tox - tox

View file

@ -1,13 +1,18 @@
FROM alpine FROM python:3.10-alpine
# if omitted, the versions are determined from the git tags
ARG tor_version
ARG torsocks_version
ENV HOME /var/lib/tor ENV HOME /var/lib/tor
ENV POETRY_VIRTUALENVS_CREATE=false
RUN apk add --no-cache git libevent-dev openssl-dev gcc make automake ca-certificates autoconf musl-dev coreutils zlib zlib-dev && \ RUN apk add --no-cache git bind-tools cargo libevent-dev openssl-dev gnupg gcc make automake ca-certificates autoconf musl-dev coreutils libffi-dev zlib-dev && \
mkdir -p /usr/local/src/ && \ mkdir -p /usr/local/src/ /var/lib/tor/ && \
git clone https://git.torproject.org/tor.git /usr/local/src/tor && \ git clone https://git.torproject.org/tor.git /usr/local/src/tor && \
cd /usr/local/src/tor && \ cd /usr/local/src/tor && \
git checkout $(git branch -a | grep 'release' | sort -V | tail -1) && \ TOR_VERSION=${tor_version=$(git tag | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)} && \
head ReleaseNotes | grep version | awk -F"version" '{print $2}' | grep - | awk '{ print $1 }' > /version && \ git checkout tor-$TOR_VERSION && \
./autogen.sh && \ ./autogen.sh && \
./configure \ ./configure \
--disable-asciidoc \ --disable-asciidoc \
@ -16,27 +21,20 @@ RUN apk add --no-cache git libevent-dev openssl-dev gcc make automake ca-cer
make && make install && \ make && make install && \
cd .. && \ cd .. && \
rm -rf tor && \ rm -rf tor && \
apk add --no-cache python3 python3-dev && \ pip3 install --upgrade pip poetry && \
python3 -m ensurepip && \ apk del git libevent-dev openssl-dev gnupg cargo make automake autoconf musl-dev coreutils libffi-dev && \
rm -r /usr/lib/python*/ensurepip && \
pip3 install --upgrade pip setuptools pycrypto && \
apk del git libevent-dev openssl-dev make automake python3-dev gcc autoconf musl-dev coreutils && \
apk add --no-cache libevent openssl apk add --no-cache libevent openssl
RUN mkdir -p /etc/tor/
ADD assets/entrypoint-config.yml /
ADD assets/onions /usr/local/src/onions
ADD assets/torrc /var/local/tor/torrc.tpl
ADD assets/v3onions /usr/bin/v3onions
RUN chmod +x /usr/bin/v3onions
RUN cd /usr/local/src/onions && python3 setup.py install
RUN mkdir -p ${HOME}/.tor && \ RUN mkdir -p ${HOME}/.tor && \
addgroup -S -g 107 tor && \ addgroup -S -g 107 tor && \
adduser -S -G tor -u 104 -H -h ${HOME} tor adduser -S -G tor -u 104 -H -h ${HOME} tor
COPY assets/entrypoint-config.yml /
COPY assets/torrc /var/local/tor/torrc.tpl
COPY assets/vanguards.conf.tpl /var/local/tor/vanguards.conf.tpl
ENV VANGUARDS_CONFIG /etc/tor/vanguards.conf
VOLUME ["/var/lib/tor/hidden_service/"] VOLUME ["/var/lib/tor/hidden_service/"]
ENTRYPOINT ["pyentrypoint"] ENTRYPOINT ["pyentrypoint"]

View file

@ -1,23 +1,60 @@
.EXPORT_ALL_VARIABLES:
LAST_TOR_VERSION = $(shell bash last_tor_version.sh)
LAST_TORSOCKS_VERSION = $(shell bash last_torsocks_version.sh)
TOR_VERSION = $(shell cat current_tor_version)
TORSOCKS_VERSION = $(shell cat current_torsocks_version)
CUR_COMMIT = $(shell git rev-parse --short HEAD)
CUR_TAG = v$(TOR_VERSION)-$(CUR_COMMIT)
test: test:
tox tox
tag:
git tag $(CUR_TAG)
update_tor_version:
echo $(LAST_TOR_VERSION) > current_tor_version
echo $(LAST_TORSOCKS_VERSION) > current_torsocks_version
release: test tag
git push origin --tags
check: check:
pre-commit run --all-files pre-commit run --all-files
build: build:
docker-compose build - echo build with tor version $(TOR_VERSION) and torsocks version $(TORSOCKS_VERSION)
- echo 'Please run make update_tor_version to build the container with the last tor version'
docker-compose -f docker-compose.build.yml build
rebuild:
- echo rebuild with tor version $(TOR_VERSION) and torsocks version $(TORSOCKS_VERSION)
- echo 'Please run make update_tor_version to build the container with the last tor version'
docker-compose -f docker-compose.build.yml build --no-cache --pull
run: build run: build
docker-compose up docker-compose -f docker-compose.v2.yml up --force-recreate
build-v2: run-v2-socket: build
docker-compose -f docker-compose.v2.yml build docker-compose -f docker-compose.v2.socket.yml up --force-recreate
run-v2: build-v2 run-v3: build
docker-compose -f docker-compose.v2.yml up docker-compose -f docker-compose.v3.yml up --force-recreate
build-v3: shell-v3: build
docker-compose -f docker-compose.v3.yml build docker-compose -f docker-compose.v3.yml run tor--rm tor sh
run-v3: build-v3 run-v3-latest:
docker-compose -f docker-compose.v3.yml up docker-compose -f docker-compose.v3.latest.yml up --force-recreate
run-vanguards: build
docker-compose -f docker-compose.vanguards.yml up --force-recreate
run-vanguards-network: build
docker-compose -f docker-compose.vanguards-network.yml up --force-recreate
publish: build
docker tag goldy/tor-hidden-service:$(CUR_TAG) goldy/tor-hidden-service:latest
docker push goldy/tor-hidden-service:$(CUR_TAG)
docker push goldy/tor-hidden-service:latest

247
README.md
View file

@ -5,136 +5,121 @@
* /version is a text file with the current of Tor version generated with each build * /version is a text file with the current of Tor version generated with each build
* Weekly builds. The Goldy's original image hadn't been updated in some time. Using the latest version of Tor is always best practice. * Weekly builds. The Goldy's original image hadn't been updated in some time. Using the latest version of Tor is always best practice.
Create a tor hidden service with a link ## Changelog
```sh * 26 jul 2022
# run a container with a network application * Update `onions` tool to v0.7.1:
$ docker run -d --name hello_world tutum/hello-world * Fix an issue when restarting a container with control port enabled
* Updated to python 3.10
* Fix a typo in `docker-compose.vanguards-network.yml`, it works now
* Update `tor` to `0.4.7.8`
# and just link it to this container * 23 dec 2021
$ docker run -ti --link hello_world goldy/tor-hidden-service * Update `onions` tool to v0.7.0:
``` * Drop support of onion v2 adresses as tor network does not accept them anymore
* Update `tor` to `0.4.6.9`
The .onion URLs are displayed to stdout at startup.
To keep onion keys, just mount volume `/var/lib/tor/hidden_service/`
```sh
$ docker run -ti --link something --volume /path/to/keys:/var/lib/tor/hidden_service/ goldy/tor-hidden-service
```
Look at the `docker-compose.yml` file to see how to use it.
## Setup ## Setup
### Set private key ### Setup hosts
Private key is settable by environment or by copying file in `hostname/private_key` in docket volume (`hostname` is the link name). From 2019, new conf to handle tor v3 address has been added. Here an example with `docker-compose` v2+:
It's easier to pass key in environment with `docker-compose`.
```yaml ```yaml
version: "2"
services:
tor:
image: goldy/tor-hidden-service:0.3.5.8
links: links:
- hello - hello
- world - world
- again
environment: environment:
# Set private key
HELLO_KEY: |
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C
NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH
dnFnHEcsllSEqD1hPAAvMUWwSMJaNmBEFtl8DUMS9tPX5fWGX4w5Xx8dZwIDAQAB
AoGBAMb20jMHxaZHWg2qTRYYJa8LdHgS0BZxkWYefnBUbZn7dOz7mM+tddpX6raK
8OSqyQu3Tc1tB9GjPLtnVr9KfVwhUVM7YXC/wOZo+u72bv9+4OMrEK/R8xy30XWj
GePXEu95yArE4NucYphxBLWMMu2E4RodjyJpczsl0Lohcn4BAkEA+XPaEKnNA3AL
1DXRpSpaa0ukGUY/zM7HNUFMW3UP00nxNCpWLSBmrQ56Suy7iSy91oa6HWkDD/4C
k0HslnMW5wJBANdz4ehByMJZmJu/b5y8wnFSqep2jmJ1InMvd18BfVoBTQJwGMAr
+qwSwNXXK2YYl9VJmCPCfgN0o7h1AEzvdYECQAM5UxUqDKNBvHVmqKn4zShb1ugY
t1RfS8XNbT41WhoB96MT9P8qTwlniX8UZiwUrvNp1Ffy9n4raz8Z+APNwvsCQQC9
AuaOsReEmMFu8VTjNh2G+TQjgvqKmaQtVNjuOgpUKYv7tYehH3P7/T+62dcy7CRX
cwbLaFbQhUUUD2DCHdkBAkB6CbB+qhu67oE4nnBCXllI9EXktXgFyXv/cScNvM9Y
FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ
-----END RSA PRIVATE KEY-----
# hello and again will share the same onion v3 address
SERVICE1_TOR_SERVICE_HOSTS: 88:hello:80,8000:world:80
# Optional as tor version 2 is not supported anymore
SERVICE1_TOR_SERVICE_VERSION: '3'
# tor v3 address private key base 64 encoded
SERVICE1_TOR_SERVICE_KEY: |
PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++
j96H1X/gq14NwLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM
world:
image: tutum/hello-world
hostname: world
hello:
image: tutum/hello-world
hostname: hello
``` ```
Options are set using the following pattern: `LINKNAME_KEY` This configuration will output:
### Setup port ```
service1: xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:88, xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:8000
```
`xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:88` will hit `again:80`.
`xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:8000` will hit `wold:80`.
__Caution__: Using `PORT_MAP` with multiple ports on single service will cause `tor` to fail. #### Environment variables
Use link setting in environment with the following pattern: `LINKNAME_PORTS`. ##### `{SERVICE}_TOR_SERVICE_HOSTS`
Like docker, first port is exposed port and the second one is service internal port. The config patern for this variable is: `{exposed_port}:{hostname}:{port}}`
For example `80:hello:8080` will expose an onion service on port 80 to the port 8080 of hello hostname.
Unix sockets are supported too, `80:unix://path/to/socket.sock` will expose an onion service on port 80 to the socket `/path/to/socket.sock`. See `docker-compose.v2.socket.yml` for an example.
You can concatenate services using comas.
> **WARNING**: Using sockets and ports in the same service group can lead to issues
##### `{SERVICE}_TOR_SERVICE_VERSION`
Optionnal now, can only be `3`. Set the tor address type.
> **WARNING**: Version 2 is not supported anymore by tor network
`2` was giving short addresses `5azvyr7dvvr4cldn.onion` and `3` gives long addresses `xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion`
##### `{SERVICE}_TOR_SERVICE_KEY`
You can set the private key for the current service.
Tor v3 addresses uses ed25519 binary keys. It should be base64 encoded:
```
PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++j96H1X/gq14NwLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM
```
##### `TOR_SOCKS_PORT`
Set tor sock5 proxy port for this tor instance. (Use this if you need to connect to tor network with your service)
##### `TOR_EXTRA_OPTIONS`
Add any options in the `torrc` file.
```yaml ```yaml
links: services:
- hello tor:
- world
- hey
environment: environment:
# Set mapping ports # Add any option you need
HELLO_PORTS: 80:80 TOR_EXTRA_OPTIONS: |
HiddenServiceNonAnonymousMode 1
# Multiple ports can be coma separated HiddenServiceSingleHopMode 1
WORLD_PORTS: 8000:80,8888:80,22:22
# Socket mapping is supported
HEY_PORTS: 80:unix:/var/run/socket.sock
``` ```
__DEPRECATED:__
By default, ports are the same as linked containers, but a default port can be mapped using `PORT_MAP` environment variable.
#### Socket
To increase security, it's possible to setup your service through socket between containers and turn off network in your app container. See `docker-compose.v2.sock.yml` for an example.
__Warning__: Due to a bug in `tor` configuration parser, it's not possible to mix network link and socket link in the same `tor` configuration.
### Group services
Multiple services can be hosted behind the same onion address.
```yaml
links:
- hello
- world
- hey
environment:
# Set mapping ports
HELLO_PORTS: 80:80
# Multiple ports can be coma separated
WORLD_PORTS: 8000:80,8888:80,22:22
# Socket mapping is supported
HEY_PORTS: 80:unix:/var/run/socket.sock
# hello and world will share the same onion address
# Service name can be any string as long there is not special char
HELLO_SERVICE_NAME: foo
WORLD_SERVICE_NAME: foo
```
__Warning__: Be carefull to not use the same exposed ports for grouped services.
### Compose v2 support
Links setting are required when using docker-compose v2. See `docker-compose.v2.yml` for example.
### Copose v3 support and secrets
Links setting are required when using docker-compose v3. See `docker-compose.v3.yml` for example.
#### Secrets #### Secrets
Secret key can be set through docker `secrets`, see `docker-compose.v3.yml` for example. Secret key can be set through docker `secrets`, see `docker-compose.v3.yml` for example.
### Tools ### Tools
A command line tool `onions` is available in container to get `.onion` url when container is running. A command line tool `onions` is available in container to get `.onion` url when container is running.
@ -142,23 +127,73 @@ A command line tool `onions` is available in container to get `.onion` url when
```sh ```sh
# Get services # Get services
$ docker exec -ti torhiddenproxy_tor_1 onions $ docker exec -ti torhiddenproxy_tor_1 onions
hello: vegm3d7q64gutl75.onion:80 hello: xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:80
world: b2sflntvdne63amj.onion:80 world: ootceq7skq7qpvvwf2tajeboxovalco7z3ka44vxbtfdr2tfvx5ld7ad.onion:80
# Get json # Get json
$ docker exec -ti torhiddenproxy_tor_1 onions --json $ docker exec -ti torhiddenproxy_tor_1 onions --json
{"hello": ["b2sflntvdne63amj.onion:80"], "world": ["vegm3d7q64gutl75.onion:80"]} {"hello": ["xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:80"], "world": ["ootceq7skq7qpvvwf2tajeboxovalco7z3ka44vxbtfdr2tfvx5ld7ad.onion:80"]}
``` ```
### Auto reload ### Auto reload
Changing `/etc/tor/torrc` file trigger a `SIGHUP` signal to `tor` to reload configuration. Changing `/etc/tor/torrc` file triggers a `SIGHUP` signal to `tor` to reload configuration.
To disable this behavior, add `ENTRYPOINT_DISABLE_RELOAD` in environment. To disable this behavior, add `ENTRYPOINT_DISABLE_RELOAD` in environment.
### Versions
Container version will follow tor release versions.
### pyentrypoint ### pyentrypoint
This container is using [`pyentrypoint`](https://github.com/cmehay/pyentrypoint) to generate its setup. This container uses [`pyentrypoint`](https://github.com/cmehay/pyentrypoint) to generate its setup.
If you need to use the legacy version, please checkout the `legacy` branch or pull `goldy/tor-hidden-service:legacy`. ### pytor
This containner uses [`pytor`](https://github.com/cmehay/pytor) to mannages tor cryptography, generate keys and compute onion urls.
## Control port
Use these environment variables to enable control port
* `TOR_CONTROL_PORT`: enable and set control port binding (`ip`, `ip:port` or `unix:/path/to/socket.sock`) (default port is 9051)
* `TOR_CONTROL_PASSWORD`: set control port password (in clear, not hashed)
* `TOR_DATA_DIRECTORY`: set data directory (default `/run/tor/data`)
## Vanguards
For critical hidden services, it's possible to increase security with [`Vanguards`](https://github.com/mikeperry-tor/vanguards) tool.
### Run in the same container
Check out [`docker-compose.vanguards.yml`](docker-compose.vanguards.yml) for example.
Add environment variable `TOR_ENABLE_VANGUARDS` to `true` to start `vanguards` daemon beside `tor` process. `Vanguards` logs will be displayed to stdout using `pyentrypoint` logging, if you need raw output, set `ENTRYPOINT_RAW` to `true` in environment.
In this mode, if `vanguards` exits, sigint is sent to `tor` process to terminate it. If you want to disable this behavior, set `VANGUARD_KILL_TOR_ON_EXIT` to `false` in environment.
### Run in separate containers
Check out[`docker-compose.vanguards-network.yml`](docker-compose.vanguards-network.yml) for an example of increased security setup using docker networks.
#### settings
Use the same environment variable as `tor` to configure `vangards` (see upper).
* `TOR_CONTROL_PORT`
* `TOR_CONTROL_PASSWORD`
##### more settings
Use `VANGUARDS_EXTRA_OPTIONS` environment variable to change any settings.
The following settings cannot me changer with this variable:
- `control_ip`:
- use `TOR_CONTROL_PORT`
- `control_port`:
- use `TOR_CONTROL_PORT`
- `control_socket`:
- use `TOR_CONTROL_PORT`
- `control_pass`:
- use `TOR_CONTROL_PASSWORD`
- `state_file`:
- use `VANGUARDS_STATE_FILE`

View file

@ -1,4 +1,6 @@
command: tor commands:
- tor
- vanguards
user: tor user: tor
group: tor group: tor
@ -7,13 +9,31 @@ secret_env:
- '*_KEY' - '*_KEY'
- '*_PORTS' - '*_PORTS'
- '*_SERVICE_NAME' - '*_SERVICE_NAME'
- '*_TOR_SERVICE_*'
- 'TOR_SOCKS_PORT'
- TOR_CONTROL_PASSWORD
config_files:
- vanguards:
- /var/local/tor/vanguards.conf.tpl: /etc/tor/vanguards.conf
pre_conf_commands: pre_conf_commands:
- tor:
- onions --setup-hosts - onions --setup-hosts
post_conf_commands: post_conf_commands:
- chown -R tor:tor $HOME - tor:
- mkdir -p /run/tor
- chmod -R 700 $HOME /run/tor
- chown -R tor:tor $HOME /run/tor
post_run_commands:
- tor:
- onions --run-vanguards
set_environment:
- vanguards:
- TOR_CONTROL_PORT: onions --resolve-control-port
reload: reload:
files: files:
- /etc/tor/torrc - /etc/tor/torrc

View file

@ -1,284 +0,0 @@
#!/usr/bin/env python3
import argparse
import logging
import os
import sys
from json import dumps
from re import match
from jinja2 import Environment
from jinja2 import FileSystemLoader
from pyentrypoint import DockerLinks
from .Service import Service
from .Service import ServicesGroup
class Setup(object):
hidden_service_dir = "/var/lib/tor/hidden_service/"
torrc = '/etc/tor/torrc'
torrc_template = '/var/local/tor/torrc.tpl'
def _add_host(self, host):
if host not in self.setup:
self.setup[host] = {}
def _get_ports(self, host, ports):
self._add_host(host)
if 'ports' not in self.setup[host]:
self.setup[host]['ports'] = {host: []}
if host not in self.setup[host]['ports']:
self.setup[host]['ports'][host] = []
ports_l = [
[
int(v) if not v.startswith('unix:') else v
for v in sp.split(':', 1)
] for sp in ports.split(',')
]
for port in ports_l:
assert len(port) == 2
if port not in self.setup[host]['ports'][host]:
self.setup[host]['ports'][host].append(port)
def _get_key(self, host, key):
self._add_host(host)
assert len(key) > 800
self.setup[host]['key'] = key
def _get_service(self, host, service):
self._add_host(host)
self.setup[host]['service'] = service
def find_group_by_service(self, service):
for group in self.services:
if service in group.services:
return group
def find_group_by_name(self, name):
for group in self.services:
if name == group.name:
return group
def find_service_by_host(self, host):
for group in self.services:
service = group.get_service_by_host(host)
if service:
return service
def add_empty_group(self, name):
if self.find_group_by_name(name):
raise Exception('Group {name} already exists'.format(name=name))
group = ServicesGroup(name=name)
self.services.append(group)
return group
def add_new_service(self, host, name=None, ports=None, key=None):
group = self.find_group_by_name(name)
service = self.find_service_by_host(host)
if not service:
service = Service(host=host)
if not group:
group = ServicesGroup(
service=service,
name=name,
hidden_service_dir=self.hidden_service_dir
)
else:
group.add_service(service)
if group not in self.services:
self.services.append(group)
else:
group = self.find_group_by_service(service)
if key:
group.add_key(key)
if ports:
service.add_ports(ports)
return service
def _set_service_names(self):
'Create groups for services, should be run first'
reg = r'([A-Z0-9]*)_SERVICE_NAME'
for key, val in os.environ.items():
m = match(reg, key)
if m:
self.add_new_service(host=m.groups()[0].lower(), name=val)
def _set_ports(self, host, ports):
self.add_new_service(host=host, ports=ports)
def _set_key(self, host, key):
self.add_new_service(host=host, key=key)
def _setup_from_env(self):
match_map = (
(r'([A-Z0-9]*)_PORTS', self._set_ports),
(r'([A-Z0-9]*)_KEY', self._set_key),
)
for key, val in os.environ.items():
for reg, call in match_map:
m = match(reg, key)
if m:
call(m.groups()[0].lower(), val)
def _get_setup_from_env(self):
self._set_service_names()
self._setup_from_env()
def _get_setup_from_links(self):
containers = DockerLinks().to_containers()
if not containers:
return
for container in containers:
host = container.names[0]
self.add_new_service(host=host)
for link in container.links:
if link.protocol != 'tcp':
continue
port_map = os.environ.get('PORT_MAP')
self._set_ports(host, '{exposed}:{internal}'.format(
exposed=port_map or link.port,
internal=link.port,
))
def apply_conf(self):
self._write_keys()
self._write_torrc()
def _write_keys(self):
for service in self.services:
service.write_key()
def _write_torrc(self):
env = Environment(loader=FileSystemLoader('/'))
temp = env.get_template(self.torrc_template)
with open(self.torrc, mode='w') as f:
f.write(temp.render(services=self.services,
env=os.environ,
type=type,
int=int))
def setup_hosts(self):
self.setup = {}
self._get_setup_from_env()
self._get_setup_from_links()
self.check_services()
self.apply_conf()
def check_services(self):
for group in self.services:
for service in group.services:
if not service.ports:
raise Exception(
'Service {name} has not ports set'.format(
name=service.host
)
)
if len(group.services) > 1 and [
True for p in service.ports if p.is_socket
]:
raise Exception(
'Cannot use socket and ports '
'in the same service'.format(
name=service.host
)
)
if len(set(dict(group)['urls'])) != len(dict(group)['urls']):
raise Exception(
'Same port for multiple services in {name} group'.format(
name=group.name
)
)
class Onions(Setup):
"""Onions"""
def __init__(self):
self.services = []
if 'HIDDEN_SERVICE_DIR' in os.environ:
self.hidden_service_dir = os.environ['HIDDEN_SERVICE_DIR']
def torrc_parser(self):
def parse_dir(line):
_, path = line.split()
group_name = os.path.basename(path)
group = (self.find_group_by_name(group_name)
or self.add_empty_group(group_name))
return group
def parse_port(line, service_group):
_, port_from, dest = line.split()
service_host, port = dest.split(':')
ports_str = '{port_from}:{dest}'
name = service_host
ports_param = ports_str.format(port_from=port_from,
dest=port)
if port.startswith('/'):
name = service_group.name
ports_param = ports_str.format(port_from=port_from,
dest=dest)
service = (service_group.get_service_by_host(name)
or Service(name))
service.add_ports(ports_param)
if service not in service_group.services:
service_group.add_service(service)
if not os.path.exists(self.torrc):
return
with open(self.torrc, 'r') as f:
for line in f.readlines():
if line.startswith('HiddenServiceDir'):
service_group = parse_dir(line)
if line.startswith('HiddenServicePort'):
parse_port(line, service_group)
def __str__(self):
if not self.services:
return 'No onion site'
return '\n'.join([str(service) for service in self.services])
def to_json(self):
service_lst = [dict(service) for service in self.services]
return dumps({
service['name']: service['urls'] for service in service_lst
})
def main():
logging.basicConfig()
parser = argparse.ArgumentParser(description='Display onion sites',
prog='onions')
parser.add_argument('--json', dest='json', action='store_true',
help='serialize to json')
parser.add_argument('--setup-hosts', dest='setup', action='store_true',
help='Setup hosts')
args = parser.parse_args()
try:
onions = Onions()
if args.setup:
onions.setup_hosts()
else:
onions.torrc_parser()
except BaseException as e:
error_msg = str(e)
else:
error_msg = None
if args.json:
if error_msg:
print(dumps({'error': error_msg}))
sys.exit(1)
logging.getLogger().setLevel(logging.ERROR)
print(onions.to_json())
else:
if error_msg:
logging.error(error_msg)
sys.exit(1)
print(onions)
if __name__ == '__main__':
main()

View file

@ -1,45 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from setuptools import find_packages
from setuptools import setup
setup(
name='onions',
version='0.4.1',
packages=find_packages(),
author="Christophe Mehay",
author_email="cmehay@nospam.student.42.fr",
description="Display onion sites hosted",
include_package_data=True,
url='http://github.com/cmehay/docker-tor-hidden-service',
classifiers=[
"Programming Language :: Python",
"Development Status :: 1 - Planning",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Topic :: System :: Installation/Setup",
],
install_requires=['pyentrypoint==0.5.1',
'Jinja2>=2.8',
'pycrypto', ],
entry_points={
'console_scripts': [
'onions = onions:main',
],
},
license="WTFPL",
)

View file

@ -1,408 +0,0 @@
import json
import os
import re
from base64 import b32encode
from hashlib import sha1
import pytest
from Crypto.PublicKey import RSA
from onions import Onions
def get_key_and_onion():
key = '''
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCsMP4gl6g1Q313miPhb1GnDr56ZxIWGsO2PwHM1infkbhlBakR
6DGQfpE31L1ZKTUxY0OexKbW088v8qCOfjD9Zk1i80JP4xzfWQcwFZ5yM/0fkhm3
zLXqXdEahvRthmFsS8OWusRs/04U247ryTm4k5S0Ch5OTBuvMLzQ8W0yDwIDAQAB
AoGAAZr3U5B2ZgC6E7phKUHjbf5KMlPxrDkVqAZQWvuIKmhuYqq518vlYmZ7rhyS
o1kqAMrfH4TP1WLmJJlLe+ibRk2aonR4e0GbW4x151wcJdT1V3vdWAsVSzG3+dqX
PiGT//DIe0OPSH6ecI8ftFRLODd6f5iGkF4gsUSTcVzAFgkCQQDTY67dRpOD9Ozw
oYH48xe0B9NQCw7g4NSH85jPurJXnpn6lZ6bcl8x8ioAdgLyomR7fO/dJFYLw6uV
LZLqZsVbAkEA0Iei3QcpsJnYgcQG7l5I26Sq3LwoiGRDFKRI6k0e+en9JQJgA3Ay
tsLpyCHv9jQ762F6AVXFru5DmZX40F6AXQJBAIHoKac8Xx1h4FaEuo4WPkPZ50ey
dANIx/OAhTFrp3vnMPNpDV60K8JS8vLzkx4vJBcrkXDSirqSFhkIN9grLi8CQEO2
l5MQPWBkRKK2pc2Hfj8cdIMi8kJ/1CyCwE6c5l8etR3sbIMRTtZ76nAbXRFkmsRv
La/7Syrnobngsh/vX90CQB+PSSBqiPSsK2yPz6Gsd6OLCQ9sdy2oRwFTasH8sZyl
bhJ3M9WzP/EMkAzyW8mVs1moFp3hRcfQlZHl6g1U9D8=
-----END RSA PRIVATE KEY-----
'''
onion = b32encode(
sha1(
RSA.importKey(
key.strip()
).publickey().exportKey(
"DER"
)[22:]
).digest()[:10]
).decode().lower() + '.onion'
return key.strip(), onion
def get_torrc_template():
return r'''
{% for service_group in services %}
HiddenServiceDir /var/lib/tor/hidden_service/{{service_group.name}}
{% for service in service_group.services %}
{% for port in service.ports %}
{% if port.is_socket %}
HiddenServicePort {{port.port_from}} {{port.dest}}
{% endif %}
{% if not port.is_socket %}
HiddenServicePort {{port.port_from}} {{service.host}}:{{port.dest}}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
{% if 'RELAY' in env %}
ORPort 9001
{% endif %}
SocksPort 0
# useless line for Jinja bug
'''.strip()
def test_ports(monkeypatch):
env = {
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '80:80,81:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
assert len(os.environ) == 3
assert len(onion.services) == 3
check = 0
for service_group in onion.services:
assert len(service_group.services) == 1
service = service_group.services[0]
if service.host == 'service1':
check += 1
assert len(service.ports) == 1
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 80
assert not service.ports[0].is_socket
if service.host == 'service2':
check += 3
assert len(service.ports) == 2
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 80
assert service.ports[1].port_from == 81
assert service.ports[1].dest == 8000
if service.host == 'service3':
check += 6
assert len(service.ports) == 1
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 'unix://unix.socket'
assert service.ports[0].is_socket
assert check == 10
def test_docker_links(fs, monkeypatch):
env = {
'HOSTNAME': 'test_env',
'COMPOSE_SERVICE1_1_PORT': 'tcp://172.17.0.2:80',
'COMPOSE_SERVICE1_1_PORT_80_TCP': 'tcp://172.17.0.2:80',
'COMPOSE_SERVICE1_1_PORT_80_TCP_ADDR': '172.17.0.2',
'COMPOSE_SERVICE1_1_PORT_80_TCP_PORT': '80',
'COMPOSE_SERVICE1_1_PORT_80_TCP_PROTO': 'tcp',
'COMPOSE_SERVICE1_1_PORT_8000_TCP': 'tcp://172.17.0.2:8000',
'COMPOSE_SERVICE1_1_PORT_8000_TCP_ADDR': '172.17.0.2',
'COMPOSE_SERVICE1_1_PORT_8000_TCP_PORT': '8000',
'COMPOSE_SERVICE1_1_PORT_8000_TCP_PROTO': 'tcp',
'COMPOSE_SERVICE1_1_NAME': '/compose_env_1/compose_service1_1',
'SERVICE1_PORT': 'tcp://172.17.0.2:80',
'SERVICE1_PORT_80_TCP': 'tcp://172.17.0.2:80',
'SERVICE1_PORT_80_TCP_ADDR': '172.17.0.2',
'SERVICE1_PORT_80_TCP_PORT': '80',
'SERVICE1_PORT_80_TCP_PROTO': 'tcp',
'SERVICE1_PORT_8000_TCP': 'tcp://172.17.0.2:8000',
'SERVICE1_PORT_8000_TCP_ADDR': '172.17.0.2',
'SERVICE1_PORT_8000_TCP_PORT': '8000',
'SERVICE1_PORT_8000_TCP_PROTO': 'tcp',
'SERVICE1_NAME': '/compose_env_1/service1',
'SERVICE1_1_PORT': 'tcp://172.17.0.2:80',
'SERVICE1_1_PORT_80_TCP': 'tcp://172.17.0.2:80',
'SERVICE1_1_PORT_80_TCP_ADDR': '172.17.0.2',
'SERVICE1_1_PORT_80_TCP_PORT': '80',
'SERVICE1_1_PORT_80_TCP_PROTO': 'tcp',
'SERVICE1_1_PORT_8000_TCP': 'tcp://172.17.0.2:8000',
'SERVICE1_1_PORT_8000_TCP_ADDR': '172.17.0.2',
'SERVICE1_1_PORT_8000_TCP_PORT': '8000',
'SERVICE1_1_PORT_8000_TCP_PROTO': 'tcp',
'SERVICE1_1_NAME': '/compose_env_1/service1_1',
}
etc_host = '''
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 service1 bf447f22cdba compose_service1_1
172.17.0.2 service1_1 bf447f22cdba compose_service1_1
172.17.0.2 compose_service1_1 bf447f22cdba
'''.strip()
fs.CreateFile('/etc/hosts', contents=etc_host)
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_links()
assert len(onion.services) == 1
group = onion.services[0]
assert len(group.services) == 1
service = group.services[0]
assert len(service.ports) == 2
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 80), (8000, 8000)])
def test_key(monkeypatch):
key, onion_url = get_key_and_onion()
env = {
'SERVICE1_KEY': key
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
assert len(os.environ) == 1
assert len(onion.services) == 1
assert onion.services[0].onion_url == onion_url
def test_key_in_secret(fs, monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
key, onion_url = get_key_and_onion()
fs.CreateFile('/run/secrets/group1', contents=key)
onion = Onions()
onion._get_setup_from_env()
group1 = onion.find_group_by_name('group1')
group2 = onion.find_group_by_name('group2')
# assert group._priv_key == key
assert group1.onion_url == onion_url
assert group2.onion_url != onion_url
def test_configuration(fs, monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
monkeypatch.setattr(os, 'fchmod', lambda x, y: None)
key, onion_url = get_key_and_onion()
torrc_tpl = get_torrc_template()
fs.CreateFile('/var/local/tor/torrc.tpl', contents=torrc_tpl)
fs.CreateFile('/etc/tor/torrc')
onion = Onions()
onion._get_setup_from_env()
onion.apply_conf()
with open('/etc/tor/torrc', 'r') as f:
torrc = f.read()
assert 'HiddenServiceDir /var/lib/tor/hidden_service/group1' in torrc
assert 'HiddenServicePort 80 service1:80' in torrc
assert 'HiddenServicePort 81 service2:80' in torrc
assert 'HiddenServicePort 82 service2:8000' in torrc
assert 'HiddenServiceDir /var/lib/tor/hidden_service/group2' in torrc
assert 'HiddenServicePort 80 unix://unix.socket' in torrc
# Check parser
onion2 = Onions()
onion2.torrc_parser()
assert len(onion2.services) == 2
assert set(
group.name for group in onion2.services
) == set(['group1', 'group2'])
for group in onion2.services:
if group.name == 'group1':
assert len(group.services) == 2
assert set(
service.host for service in group.services
) == set(['service1', 'service2'])
for service in group.services:
if service.host == 'service1':
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 80)])
if service.host == 'service2':
assert len(service.ports) == 2
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(81, 80), (82, 8000)])
if group.name == 'group2':
assert len(group.services) == 1
assert set(
service.host for service in group.services
) == set(['group2'])
service = group.services[0]
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 'unix://unix.socket')])
def test_groups(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
onion_match = r'^[a-z2-7]{16}.onion$'
assert len(os.environ) == 6
assert len(onion.services) == 2
assert set(
group.name for group in onion.services
) == set(['group1', 'group2'])
for group in onion.services:
if group.name == 'group1':
assert len(group.services) == 2
assert set(
service.host for service in group.services
) == set(['service1', 'service2'])
if group.name == 'group2':
assert len(group.services) == 1
assert set(
service.host for service in group.services
) == set(['service3'])
assert re.match(onion_match, group.onion_url)
def test_json(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
onion.check_services()
jsn = json.loads(onion.to_json())
assert len(jsn) == 2
assert len(jsn['group1']) == 3
assert len(jsn['group2']) == 1
def test_output(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
for item in ['group1', 'group2', '.onion', ',']:
assert item in str(onion)
def test_not_valid_share_port(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '80:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
with pytest.raises(Exception) as excinfo:
onion.check_services()
assert 'Same port for multiple services' in str(excinfo.value)
def test_not_valid_no_services(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
with pytest.raises(Exception) as excinfo:
onion.check_services()
assert 'has not ports set' in str(excinfo.value)

View file

@ -1,5 +1,8 @@
{% for service_group in services %} {% for service_group in onion.services %}
HiddenServiceDir /var/lib/tor/hidden_service/{{service_group.name}} HiddenServiceDir {{service_group.hidden_service_dir}}
{% if service_group.version == 3 %}
HiddenServiceVersion 3
{% endif %}
{% for service in service_group.services %} {% for service in service_group.services %}
{% for port in service.ports %} {% for port in service.ports %}
{% if port.is_socket %} {% if port.is_socket %}
@ -11,12 +14,40 @@ HiddenServicePort {{port.port_from}} {{service.host}}:{{port.dest}}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
DataDirectory {{ onion.data_directory }}
{% if 'RELAY' in env %} {% if 'TOR_SOCKS_PORT' in env %}
ORPort 9001 SocksPort {{env['TOR_SOCKS_PORT']}}
{% else %}
SocksPort 0
{% endif %} {% endif %}
SocksPort 0 {% if envtobool('TOR_EXIT_RELAY', False) %}
ExitRelay 1
{% else %}
ExitRelay 0
{% endif %}
{% if onion.enable_control_port %}
{% if onion.control_socket %}
ControlPort {{onion.control_socket}}
{% endif %}
{% if not onion.control_socket %}
{% if onion.control_ip_binding.version() == 4 %}
ControlPort {{onion.control_ip_binding}}:{{ onion.control_port }}
{% endif %}
{% if onion.control_ip_binding.version() == 6 %}
ControlPort [{{onion.control_ip_binding}}]:{{ onion.control_port }}
{% endif %}
{% endif %}
{% if onion.control_hashed_password %}
HashedControlPassword {{ onion.control_hashed_password }}
{% endif %}
{% endif %}
{% if 'TOR_EXTRA_OPTIONS' in env %}
{{env['TOR_EXTRA_OPTIONS']}}
{% endif %}
# useless line for Jinja bug # useless line for Jinja bug
HiddenServiceVersion 3 HiddenServiceVersion 3

145
assets/vanguards.conf.tpl Normal file
View file

@ -0,0 +1,145 @@
## Global options
[Global]
{% if (env.get('TOR_CONTROL_PORT', '')).startswith('unix:') %}
{% set _, unix_path = env['TOR_CONTROL_PORT'].split(':', 1) %}
{% elif ':' in env.get('TOR_CONTROL_PORT', '') %}
{% set host, port = env['TOR_CONTROL_PORT'].split(':', 1) %}
{% else %}
{% set host = env.get('TOR_CONTROL_PORT') %}
{% endif %}
control_ip = {{ host or '' }}
control_port = {{ port or 9051 }}
control_socket = {{ unix_path or '' }}
control_pass = {{ env.get('TOR_CONTROL_PASSWORD', '') }}
state_file = {{ env.get('VANGUARDS_STATE_FILE', '/run/tor/data/vanguards.state') }}
{% if 'VANGUARDS_EXTRA_OPTIONS' in env %}
{% set extra_conf = ConfigParser().read_string(env['VANGUARDS_EXTRA_OPTIONS']) %}
{% if 'Global' in extra_conf %}
{% for key, val in extra_conf['Global'].items() %}
{{key}} = {{val}}
{% endfor %}
{% set _ = extra_conf.pop('Global') %}
{% endif %}
{{ extra_conf.to_string() }}
{% endif %}
{#
## Example vanguards configuration file
#
# All values below are default values and won't appear in final config file
# Original here: https://github.com/mikeperry-tor/vanguards/blob/master/vanguards-example.conf
#
# Enable/disable active vanguard update of layer2 and layer3 guards
enable_vanguards = True
# Enable/disable the bandwidth side channel detection checks:
enable_bandguards = True
# Enable/disable circuit build timeout analysis (informational only):
enable_cbtverify = False
# Enable/disable checks on Rendezvous Point overuse attacks:
enable_rendguard = True
# Close circuits upon suspected attack conditions:
close_circuits = True
# If True, we write (or update/rotate) layer2 and layer3 vanguards in torrc,
# then exit. This option disables the bandguards and rendguard defenses.
one_shot_vanguards = False
# The current loglevel:
loglevel = NOTICE
# If specified, log to this file instead of stdout:
logfile =
## Vanguards: layer1, layer2, and layer3 rotation params.
[Vanguards]
# How long to keep our layer1 guard (0 means use Tor default):
layer1_lifetime_days = 0
# The maximum amount of time to keep a layer2 guard:
max_layer2_lifetime_hours = 1080
# The maximum amount of time to keep a layer3 guard:
max_layer3_lifetime_hours = 48
# The minimum amount of time to keep a layer2 guard:
min_layer2_lifetime_hours = 24
# The minimum amount of time to keep a layer3 guard:
min_layer3_lifetime_hours = 1
# The number of layer1 guards:
num_layer1_guards = 2
# The number of layer2 guards:
num_layer2_guards = 3
# The number of layer3 guards:
num_layer3_guards = 8
## Bandguards: Mechanisms to detect + mitigate bandwidth side channel attacks.
[Bandguards]
# Maximum number of hours to allow any circuit to remain open
# (set to 0 to disable):
circ_max_age_hours = 24
# Maximum amount of kilobytes that can be present in a hidden service
# descriptor before we close the circuit (set to 0 to disable):
circ_max_hsdesc_kilobytes = 30
# Total maximum megabytes on any circuit before we close it. Note that
# while HTTP GET can resume if this limit is hit, HTTP POST will not.
# This means that applications that require large data submission (eg
# SecureDrop or onionshare) should set this much higher
# (or set to 0 to disable):
circ_max_megabytes = 0
# Warn if we can't build or use circuits for this many seconds.
circ_max_disconnected_secs = 30
# Warn if we are disconnected from the Tor network for this many seconds.
conn_max_disconnected_secs = 15
## Rendguard: Monitors service-side Rendezvous Points to detect misuse/attack
[Rendguard]
# No relay should show up as a Rendezvous Point more often than this ratio
# multiplied by its bandwidth weight:
rend_use_max_use_to_bw_ratio = 5.0
# What is percent of the network weight is not in the consensus right now?
# Put another way, the max number of rend requests from relays not in the
# consensus is rend_use_max_use_to_bw_ratio times this churn rate.
rend_use_max_consensus_weight_churn = 1.0
# Close circuits where the Rendezvous Point appears too often. Note that an
# adversary can deliberately cause RP overuse in order to impact availability.
# If this is a concern, either set this to false, or raise the ratio
# parameter above.
rend_use_close_circuits_on_overuse = True
# Total number of circuits we need before we begin enforcing rendezvous point
# ratio limits:
rend_use_global_start_count = 1000
# Number of times a relay must be seen as a Rendezvous Point before applying
# ratio limits:
rend_use_relay_start_count = 100
# Divide all relay counts by two once the total circuit count hits this many:
rend_use_scale_at_count = 20000
#}

1
current_tor_version Normal file
View file

@ -0,0 +1 @@
0.4.7.12

1
current_torsocks_version Normal file
View file

@ -0,0 +1 @@
v2.3.0

12
docker-compose.build.yml Normal file
View file

@ -0,0 +1,12 @@
# docker version 3 builder
version: "3.1"
services:
tor:
image: goldy/tor-hidden-service:$CUR_TAG
build:
context: .
args:
tor_version: $TOR_VERSION
torsocks_version: $TORSOCKS_VERSION

View file

@ -4,13 +4,13 @@ version: "2"
services: services:
tor: tor:
image: goldy/tor-hidden-service image: goldy/tor-hidden-service:$CUR_TAG
build: . build: .
links: links:
- world - world
environment: environment:
# Set mapping port to unix socket # Set service hosts to unix socket
WORLD_PORTS: 80:unix:/var/run/nginx.sock WORLD_TOR_SERVICE_HOSTS: 80:unix://var/run/nginx.sock
# Mount socket directory from world container # Mount socket directory from world container
volumes_from: volumes_from:

View file

@ -4,40 +4,41 @@ version: "2"
services: services:
tor: tor:
image: goldy/tor-hidden-service image: goldy/tor-hidden-service:$CUR_TAG
build: .
links: links:
- hello - hello
- world - world
- again - again
environment: environment:
# Set mapping ports ######################################################################
HELLO_PORTS: 80:80,800:80,8888:80 ### TOR ADDRESSES VERSION 2 ARE NOT SUPPORTED ANYMORE ###
# Set private key ######################################################################
HELLO_KEY: | # # Set mapping ports
-----BEGIN RSA PRIVATE KEY----- # HELLO_TOR_SERVICE_HOSTS: 80:hello:80,800:hello:80,8888:hello:80
MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C # # Set private key
NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH # HELLO_TOR_SERVICE_KEY: |
dnFnHEcsllSEqD1hPAAvMUWwSMJaNmBEFtl8DUMS9tPX5fWGX4w5Xx8dZwIDAQAB # -----BEGIN RSA PRIVATE KEY-----
AoGBAMb20jMHxaZHWg2qTRYYJa8LdHgS0BZxkWYefnBUbZn7dOz7mM+tddpX6raK # MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C
8OSqyQu3Tc1tB9GjPLtnVr9KfVwhUVM7YXC/wOZo+u72bv9+4OMrEK/R8xy30XWj # NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH
GePXEu95yArE4NucYphxBLWMMu2E4RodjyJpczsl0Lohcn4BAkEA+XPaEKnNA3AL # dnFnHEcsllSEqD1hPAAvMUWwSMJaNmBEFtl8DUMS9tPX5fWGX4w5Xx8dZwIDAQAB
1DXRpSpaa0ukGUY/zM7HNUFMW3UP00nxNCpWLSBmrQ56Suy7iSy91oa6HWkDD/4C # AoGBAMb20jMHxaZHWg2qTRYYJa8LdHgS0BZxkWYefnBUbZn7dOz7mM+tddpX6raK
k0HslnMW5wJBANdz4ehByMJZmJu/b5y8wnFSqep2jmJ1InMvd18BfVoBTQJwGMAr # 8OSqyQu3Tc1tB9GjPLtnVr9KfVwhUVM7YXC/wOZo+u72bv9+4OMrEK/R8xy30XWj
+qwSwNXXK2YYl9VJmCPCfgN0o7h1AEzvdYECQAM5UxUqDKNBvHVmqKn4zShb1ugY # GePXEu95yArE4NucYphxBLWMMu2E4RodjyJpczsl0Lohcn4BAkEA+XPaEKnNA3AL
t1RfS8XNbT41WhoB96MT9P8qTwlniX8UZiwUrvNp1Ffy9n4raz8Z+APNwvsCQQC9 # 1DXRpSpaa0ukGUY/zM7HNUFMW3UP00nxNCpWLSBmrQ56Suy7iSy91oa6HWkDD/4C
AuaOsReEmMFu8VTjNh2G+TQjgvqKmaQtVNjuOgpUKYv7tYehH3P7/T+62dcy7CRX # k0HslnMW5wJBANdz4ehByMJZmJu/b5y8wnFSqep2jmJ1InMvd18BfVoBTQJwGMAr
cwbLaFbQhUUUD2DCHdkBAkB6CbB+qhu67oE4nnBCXllI9EXktXgFyXv/cScNvM9Y # +qwSwNXXK2YYl9VJmCPCfgN0o7h1AEzvdYECQAM5UxUqDKNBvHVmqKn4zShb1ugY
FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ # t1RfS8XNbT41WhoB96MT9P8qTwlniX8UZiwUrvNp1Ffy9n4raz8Z+APNwvsCQQC9
-----END RSA PRIVATE KEY----- # AuaOsReEmMFu8VTjNh2G+TQjgvqKmaQtVNjuOgpUKYv7tYehH3P7/T+62dcy7CRX
# cwbLaFbQhUUUD2DCHdkBAkB6CbB+qhu67oE4nnBCXllI9EXktXgFyXv/cScNvM9Y
WORLD_PORTS: 8000:80 # FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ
# -----END RSA PRIVATE KEY-----
AGAIN_PORTS: 88:80
# hello and again will share the same onion_adress # hello and again will share the same onion_adress
AGAIN_SERVICE_NAME: foo FOO_TOR_SERVICE_HOSTS: 88:again:80,8000:world:80
HELLO_SERVICE_NAME: foo # tor v3 address private key base 64 encoded
FOO_TOR_SERVICE_KEY: |
PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAABYZRzL3zScTEqA8/5wfvHw
yLIzmih73lhgPGPh7SuOS6GTou4tXgNlTYSNb/Fvk1ajTTUno4tIQn/jMENO/20G
# Keep keys in volumes # Keep keys in volumes
volumes: volumes:

View file

@ -0,0 +1,55 @@
# docker version 3 example
version: "3.1"
services:
tor:
image: goldy/tor-hidden-service:latest
links:
- hello
- world
- again
environment:
# Set version 3 on BAR group
BAR_TOR_SERVICE_HOSTS: '80:hello:80,88:world:80'
# This is now optional as v2 are not supported anymore by tor network
BAR_TOR_SERVICE_VERSION: '3'
# hello and again will share the same onion_adress
FOO_TOR_SERVICE_HOSTS: '88:again:80,80:hello:80,800:hello:80,8888:hello:80'
# Keep keys in volumes
volumes:
- tor-keys:/var/lib/tor/hidden_service/
# Set secret for key, use the same name as the service
secrets:
- source: foo
target: foo
mode: 0400
- source: bar
target: bar
mode: 0400
hello:
image: tutum/hello-world
hostname: hello
world:
image: tutum/hello-world
hostname: world
again:
image: tutum/hello-world
hostname: again
volumes:
tor-keys:
driver: local
secrets:
foo:
file: ./private_key_foo_v3
bar:
file: ./private_key_bar_v3

View file

@ -4,23 +4,20 @@ version: "3.1"
services: services:
tor: tor:
image: goldy/tor-hidden-service image: goldy/tor-hidden-service:$CUR_TAG
build: .
links: links:
- hello - hello
- world - world
- again - again
environment: environment:
# Set mapping ports # Set version 3 on BAR group
HELLO_PORTS: 80:80,800:80,8888:80 BAR_TOR_SERVICE_HOSTS: '80:hello:80,88:world:80'
# This is now optional as v2 are not supported anymore by tor network
WORLD_PORTS: 8000:80 BAR_TOR_SERVICE_VERSION: '3'
AGAIN_PORTS: 88:80
# hello and again will share the same onion_adress # hello and again will share the same onion_adress
AGAIN_SERVICE_NAME: foo FOO_TOR_SERVICE_HOSTS: '88:again:80,80:hello:80,800:hello:80,8888:hello:80'
HELLO_SERVICE_NAME: foo
# Keep keys in volumes # Keep keys in volumes
volumes: volumes:
@ -28,9 +25,8 @@ services:
# Set secret for key, use the same name as the service # Set secret for key, use the same name as the service
secrets: secrets:
- source: foo - foo
target: foo - bar
mode: 0400
hello: hello:
image: tutum/hello-world image: tutum/hello-world
@ -50,4 +46,6 @@ volumes:
secrets: secrets:
foo: foo:
file: ./foo_private_key file: ./private_key_foo_v3
bar:
file: ./private_key_bar_v3

View file

@ -0,0 +1,105 @@
# Run secure vanguards using network
version: "3.1"
services:
# Tor container
tor:
image: goldy/tor-hidden-service:$CUR_TAG
environment:
# Enable control port with ip binding (see networks configuration bellow)
# Using network interface instead of 0.0.0.0 help to protect control port from hidden services.
TOR_CONTROL_PORT: 172.16.111.10
# Set controle port password (optionnal)
TOR_CONTROL_PASSWORD: something_secret
# You can change any options here, excepted control_* ones and state_file
VANGUARDS_EXTRA_OPTIONS: |
[Global]
enable_cbtverify = True
loglevel = DEBUG
HELLO_TOR_SERVICE_HOSTS: '80:hello:80'
HELLO_TOR_SERVICE_VERSION: '3'
# Keep keys in volumes
volumes:
# Keep keys in volumes
- tor-keys:/var/lib/tor/hidden_service/
- tor-data:/run/tor/data
# Set secret for key, use the same name as the service
secrets:
- source: hello
target: hello
mode: 0400
networks:
hidden_services:
ipv4_address: 172.16.222.10
tor_control:
# Set an ip address for tor_control network to bind for the good network
ipv4_address: 172.16.111.10
# Vanguards container
vanguards:
depends_on:
- tor
# Use the same image
image: goldy/tor-hidden-service:$CUR_TAG
# Run vanguards
command: vanguards
environment:
# Set tor hostname (or ip:port or unix:/path/to/socket.sock)
TOR_CONTROL_PORT: tor:9051
# set password if needed
TOR_CONTROL_PASSWORD: something_secret
# Vanguards is assigned to tor_control network
networks:
- tor_control
# Sharing tor-data volume with tor container
volumes:
- tor-data:/run/tor/data
# Hidden service container
hello:
image: tutum/hello-world
hostname: hello
depends_on:
- tor
# this hidden service is assigned to hidden_services network
networks:
- hidden_services
volumes:
tor-keys:
driver: local
tor-data:
driver: local
secrets:
hello:
file: ./private_key_bar_v3
networks:
# This network is used for hidden services
hidden_services:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.16.222.0/24
# This network is used for vagrands to get access to tor
tor_control:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.16.111.0/24

View file

@ -0,0 +1,44 @@
# Secure vangard in the same container
version: "3.1"
services:
tor:
image: goldy/tor-hidden-service:$CUR_TAG
environment:
# Enable Vanguards like this
TOR_ENABLE_VANGUARDS: 'true'
# You can change any options here, excepted control_* ones
VANGUARDS_EXTRA_OPTIONS: |
[Global]
enable_cbtverify = True
loglevel = DEBUG
HELLO_TOR_SERVICE_HOSTS: '80:hello:80'
HELLO_TOR_SERVICE_VERSION: '3'
# Keep keys in volumes
volumes:
- tor-keys:/var/lib/tor/hidden_service/
# Set secret for key, use the same name as the service
secrets:
- source: hello
target: hello
mode: 0400
hello:
image: tutum/hello-world
hostname: hello
depends_on:
- tor
volumes:
tor-keys:
driver: local
secrets:
hello:
file: ./private_key_bar_v3

View file

@ -1,19 +0,0 @@
# docker-compose.yml example
tor:
image: goldy/tor-hidden-service
links:
- hello
- world
environment:
PORT_MAP: 80 # Map port to detected service
volumes:
- ./keys:/var/lib/tor/hidden_service/
hello:
image: tutum/hello-world
hostname: hello
world:
image: tutum/hello-world
hostname: world

View file

@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C
NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH
dnFnHEcsllSEqD1hPAAvMUWwSMJaNmBEFtl8DUMS9tPX5fWGX4w5Xx8dZwIDAQAB
AoGBAMb20jMHxaZHWg2qTRYYJa8LdHgS0BZxkWYefnBUbZn7dOz7mM+tddpX6raK
8OSqyQu3Tc1tB9GjPLtnVr9KfVwhUVM7YXC/wOZo+u72bv9+4OMrEK/R8xy30XWj
GePXEu95yArE4NucYphxBLWMMu2E4RodjyJpczsl0Lohcn4BAkEA+XPaEKnNA3AL
1DXRpSpaa0ukGUY/zM7HNUFMW3UP00nxNCpWLSBmrQ56Suy7iSy91oa6HWkDD/4C
k0HslnMW5wJBANdz4ehByMJZmJu/b5y8wnFSqep2jmJ1InMvd18BfVoBTQJwGMAr
+qwSwNXXK2YYl9VJmCPCfgN0o7h1AEzvdYECQAM5UxUqDKNBvHVmqKn4zShb1ugY
t1RfS8XNbT41WhoB96MT9P8qTwlniX8UZiwUrvNp1Ffy9n4raz8Z+APNwvsCQQC9
AuaOsReEmMFu8VTjNh2G+TQjgvqKmaQtVNjuOgpUKYv7tYehH3P7/T+62dcy7CRX
cwbLaFbQhUUUD2DCHdkBAkB6CbB+qhu67oE4nnBCXllI9EXktXgFyXv/cScNvM9Y
FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ
-----END RSA PRIVATE KEY-----

5
hooks/build Normal file
View file

@ -0,0 +1,5 @@
#!/bin/bash
v1="${SOURCE_BRANCH%-*}"
tor_version=${v1:1}
docker build --build-arg tor_version=${tor_version} -f $DOCKERFILE_PATH -t $IMAGE_NAME .

4
hooks/post_push Normal file
View file

@ -0,0 +1,4 @@
#!/bin/bash
docker tag $IMAGE_NAME ${DOCKER_REPO}:latest
docker push ${DOCKER_REPO}:latest

2
last_tor_version.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
git ls-remote --tags https://git.torproject.org/tor.git | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1

2
last_torsocks_version.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
git ls-remote --tags https://git.torproject.org/torsocks.git | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1

521
onions/Onions.py Normal file
View file

@ -0,0 +1,521 @@
#!/usr/bin/env python3
import argparse
import logging
import os
import socket
import subprocess
import sys
from base64 import b64decode
from json import dumps
from re import match
from IPy import IP
from jinja2 import Environment
from jinja2 import FileSystemLoader
from pyentrypoint import DockerLinks
from pyentrypoint.config import envtobool
from pyentrypoint.configparser import ConfigParser
from .Service import Service
from .Service import ServicesGroup
class Setup(object):
hidden_service_dir = "/var/lib/tor/hidden_service/"
data_directory = "/run/tor/data"
torrc = '/etc/tor/torrc'
torrc_template = '/var/local/tor/torrc.tpl'
enable_control_port = False
control_port = 9051
control_ip_binding = IP('0.0.0.0')
control_hashed_password = None
control_socket = 'unix:/run/tor/tor_control.sock'
enable_vanguards = False
vanguards_template = '/var/local/tor/vanguards.conf.tpl'
vanguards_conf = '/etc/tor/vanguards.conf'
def _add_host(self, host):
if host not in self.setup:
self.setup[host] = {}
def _get_ports(self, host, ports):
self._add_host(host)
if 'ports' not in self.setup[host]:
self.setup[host]['ports'] = {host: []}
if host not in self.setup[host]['ports']:
self.setup[host]['ports'][host] = []
ports_l = [
[
int(v) if not v.startswith('unix:') else v
for v in sp.split(':', 1)
] for sp in ports.split(',')
]
for port in ports_l:
assert len(port) == 2
if port not in self.setup[host]['ports'][host]:
self.setup[host]['ports'][host].append(port)
def _hash_control_port_password(self, password):
self.control_hashed_password = subprocess.check_output(
['/usr/local/bin/tor', '--quiet', '--hash-password', password],
env={'HOME': '/tmp'}
).decode()
def _parse_control_port_variable(self, check_ip=True):
control_port = os.environ['TOR_CONTROL_PORT']
try:
if control_port.startswith('unix:'):
self.control_socket = control_port
return
self.control_socket = None
if ':' in control_port:
host, port = control_port.split(':')
self.control_ip_binding = IP(host) if check_ip else host
self.control_port = int(port)
return
self.control_ip_binding = (
IP(control_port) if check_ip else control_port
)
except BaseException as e:
logging.error('TOR_CONTROL_PORT environment variable error')
logging.exception(e)
def _setup_control_port(self):
if 'TOR_CONTROL_PORT' not in os.environ:
return
self.enable_control_port = True
self._parse_control_port_variable()
if os.environ.get('TOR_CONTROL_PASSWORD'):
self._hash_control_port_password(os.environ[
'TOR_CONTROL_PASSWORD'
])
if envtobool('TOR_DATA_DIRECTORY', False):
self.data_directory = os.environ['TOR_DATA_DIRECTORY']
def _setup_vanguards(self):
if not envtobool('TOR_ENABLE_VANGUARDS', False):
return
self.enable_control_port = True
self.enable_vanguards = True
os.environ.setdefault('TOR_CONTROL_PORT', self.control_socket)
self.kill_tor_on_vanguard_exit = envtobool(
'VANGUARD_KILL_TOR_ON_EXIT',
True
)
self.vanguards_state_file = os.path.join(
self.data_directory,
'vanguards.state'
)
def _get_key(self, host, key):
self._add_host(host)
assert len(key) > 800
self.setup[host]['key'] = key
def _load_keys_in_services(self, secret=True):
for service in self.services:
service.load_key(secret=secret)
def _get_service(self, host, service):
self._add_host(host)
self.setup[host]['service'] = service
def find_group_by_service(self, service):
for group in self.services:
if service in group.services:
return group
def find_group_by_name(self, name):
for group in self.services:
if name == group.name:
return group
def find_service_by_host(self, host):
for group in self.services:
service = group.get_service_by_host(host)
if service:
return service
def add_empty_group(self, name, version=None):
if self.find_group_by_name(name):
raise Exception('Group {name} already exists'.format(name=name))
group = ServicesGroup(name=name, version=version)
self.services.append(group)
return group
def add_new_service(self,
host,
name=None,
ports=None,
key=None):
group = self.find_group_by_name(name)
if group:
service = group.get_service_by_host(host)
else:
service = self.find_service_by_host(host)
if not service:
service = Service(host=host)
if not group:
group = ServicesGroup(
service=service,
name=name,
hidden_service_dir=self.hidden_service_dir,
)
else:
group.add_service(service)
if group not in self.services:
self.services.append(group)
elif group and service not in group.services:
group.add_service(service)
else:
self.find_group_by_service(service)
if key:
group.add_key(key)
if ports:
service.add_ports(ports)
return service
def _set_service_names(self):
'Create groups for services, should be run first'
reg = r'([A-Z0-9]*)_SERVICE_NAME'
for key, val in os.environ.items():
m = match(reg, key)
if m:
try:
self.add_new_service(host=m.groups()[0].lower(), name=val)
except BaseException as e:
logging.error(f"Fail to setup from {key} environment")
logging.error(e)
def _set_ports(self, host, ports):
self.add_new_service(host=host, ports=ports)
def _set_key(self, host, key):
self.add_new_service(host=host, key=key.encode())
def _setup_from_env(self, match_map):
for reg, call in match_map:
for key, val in os.environ.items():
m = match(reg, key)
# Ignore GPG_KEY env variable to avoid warning
# (this is a deprecated setup)
if m and key != 'GPG_KEY':
try:
call(m.groups()[0].lower(), val)
except BaseException as e:
logging.error(f"Fail to setup from {key} environment")
logging.error(e)
def _setup_keys_and_ports_from_env(self):
self._setup_from_env(
(
(r'([A-Z0-9]+)_PORTS', self._set_ports),
(r'([A-Z0-9]+)_KEY', self._set_key),
)
)
def get_or_create_empty_group(self, name, version=None):
group = self.find_group_by_name(name)
if group:
if version:
group.set_version(version)
return group
return self.add_empty_group(name, version)
def _set_group_version(self, name, version):
'Setup groups with version'
group = self.get_or_create_empty_group(name, version=version)
group.set_version(version)
def _set_group_key(self, name, key):
'Set key for service group'
group = self.get_or_create_empty_group(name)
if group.version == 3:
group.add_key(b64decode(key))
else:
group.add_key(key)
def _set_group_hosts(self, name, hosts):
'Set services for service groups'
self.get_or_create_empty_group(name)
for host_map in hosts.split(','):
host_map = host_map.strip()
port_from, host, port_dest = host_map.split(':', 2)
if host == 'unix' and port_dest.startswith('/'):
self.add_new_service(host=name, name=name, ports=host_map)
else:
ports = '{frm}:{dst}'.format(frm=port_from, dst=port_dest)
self.add_new_service(host=host, name=name, ports=ports)
def _setup_services_from_env(self):
self._setup_from_env(
(
(r'([A-Z0-9]+)_TOR_SERVICE_VERSION', self._set_group_version),
(r'([A-Z0-9]+)_TOR_SERVICE_KEY', self._set_group_key),
(r'([A-Z0-9]+)_TOR_SERVICE_HOSTS', self._set_group_hosts),
)
)
def _get_setup_from_env(self):
self._set_service_names()
self._setup_keys_and_ports_from_env()
self._setup_services_from_env()
def _get_setup_from_links(self):
containers = DockerLinks().to_containers()
if not containers:
return
for container in containers:
host = container.names[0]
self.add_new_service(host=host)
for link in container.links:
if link.protocol != 'tcp':
continue
port_map = os.environ.get('PORT_MAP')
self._set_ports(host, '{exposed}:{internal}'.format(
exposed=port_map or link.port,
internal=link.port,
))
def apply_conf(self):
self._write_keys()
self._write_torrc()
if self.enable_vanguards:
self._write_vanguards_conf()
def _write_keys(self):
for service in self.services:
service.write_key()
def _write_torrc(self):
env = Environment(loader=FileSystemLoader('/'))
temp = env.get_template(self.torrc_template)
with open(self.torrc, mode='w') as f:
f.write(temp.render(onion=self,
env=os.environ,
envtobool=envtobool,
type=type,
int=int))
def _write_vanguards_conf(self):
env = Environment(loader=FileSystemLoader('/'))
temp = env.get_template(self.vanguards_template)
with open(self.vanguards_conf, mode='w') as f:
f.write(temp.render(env=os.environ,
ConfigParser=ConfigParser,
envtobool=envtobool))
def run_vanguards(self):
self._setup_vanguards()
if not self.enable_vanguards:
return
logging.info('Vanguard enabled, starting...')
if not self.kill_tor_on_vanguard_exit:
os.execvp('vanguards', ['vanguards'])
try:
subprocess.check_call('vanguards')
except subprocess.CalledProcessError as e:
logging.error(str(e))
finally:
logging.error('Vanguards has exited, killing tor...')
os.kill(1, 2)
def resolve_control_hostname(self):
try:
addr = socket.getaddrinfo(self.control_ip_binding,
None,
socket.AF_INET,
socket.SOCK_STREAM,
socket.IPPROTO_TCP)
except socket.gaierror:
raise
return IP(addr[0][4][0])
def resolve_control_port(self):
if 'TOR_CONTROL_PORT' not in os.environ:
return
self._parse_control_port_variable(check_ip=False)
if self.control_socket:
print(os.environ['TOR_CONTROL_PORT'])
try:
ip = IP(self.control_ip_binding)
except ValueError:
ip = self.resolve_control_hostname()
print(f"{ip}:{self.control_port}")
def setup_hosts(self):
self.setup = {}
self._get_setup_from_env()
self._get_setup_from_links()
self._load_keys_in_services()
self.check_services()
self._setup_vanguards()
self._setup_control_port()
self.apply_conf()
def check_services(self):
to_remove = set()
for group in self.services:
try:
for service in group.services:
if not service.ports:
raise Exception(
'Service {name} has not ports set'.format(
name=service.host
)
)
if len(group.services) > 1 and [
True for p in service.ports if p.is_socket
]:
raise Exception(
f'Cannot use socket and ports '
f'in the same {service.host}'
)
if len(set(dict(group)['urls'])) != len(dict(group)['urls']):
raise Exception(
f'Same port for multiple services in '
f'{group.name} group'
)
except Exception as e:
logging.error(e)
to_remove.add(group)
for group in to_remove:
self.services.remove(group)
class Onions(Setup):
"""Onions"""
def __init__(self):
self.services = []
if 'HIDDEN_SERVICE_DIR' in os.environ:
self.hidden_service_dir = os.environ['HIDDEN_SERVICE_DIR']
if os.environ.get('TOR_DATA_DIRECTORY'):
self.data_directory = os.environ['TOR_DATA_DIRECTORY']
def torrc_parser(self):
self.torrc_dict = {}
def parse_dir(line):
_, path = line.split()
group_name = os.path.basename(path)
self.torrc_dict[group_name] = {
'services': [],
}
return group_name
def parse_port(line, name):
_, port_from, dest = line.split()
service_host, port = dest.split(':')
ports_str = '{port_from}:{dest}'
ports_param = ports_str.format(port_from=port_from,
dest=port)
if port.startswith('/'):
service_host = name
ports_param = ports_str.format(port_from=port_from,
dest=dest)
self.torrc_dict[name]['services'].append({
'host': service_host,
'ports': ports_param,
})
def parse_version(line, name):
_, version = line.split()
self.torrc_dict[name]['version'] = int(version)
def setup_services():
for name, setup in self.torrc_dict.items():
version = setup.get('version', 3)
group = (self.find_group_by_name(name)
or self.add_empty_group(name, version=version))
for service_dict in setup.get('services', []):
host = service_dict['host']
service = (group.get_service_by_host(host)
or Service(host))
service.add_ports(service_dict['ports'])
if service not in group.services:
group.add_service(service)
self._load_keys_in_services(secret=False)
if not os.path.exists(self.torrc):
return
try:
with open(self.torrc, 'r') as f:
for line in f.readlines():
if line.startswith('HiddenServiceDir'):
name = parse_dir(line)
if line.startswith('HiddenServicePort'):
parse_port(line, name)
if line.startswith('HiddenServiceVersion'):
parse_version(line, name)
except BaseException:
raise Exception(
'Fail to parse torrc file. Please check the file'
)
setup_services()
def __str__(self):
if not self.services:
return 'No onion site'
return '\n'.join([str(service) for service in self.services])
def to_json(self):
service_lst = [dict(service) for service in self.services]
return dumps({
service['name']: service['urls'] for service in service_lst
})
def main():
logging.basicConfig()
parser = argparse.ArgumentParser(description='Display onion sites',
prog='onions')
parser.add_argument('--json', dest='json', action='store_true',
help='serialize to json')
parser.add_argument('--setup-hosts', dest='setup', action='store_true',
help='Setup hosts')
parser.add_argument('--run-vanguards', dest='vanguards',
action='store_true',
help='Run Vanguards in tor container')
parser.add_argument('--resolve-control-port', dest='resolve_control_port',
action='store_true',
help='Resolve ip from host if needed')
args = parser.parse_args()
logging.getLogger().setLevel(logging.WARNING)
try:
onions = Onions()
if args.vanguards:
onions.run_vanguards()
return
if args.resolve_control_port:
onions.resolve_control_port()
return
if args.setup:
onions.setup_hosts()
else:
onions.torrc_parser()
except BaseException as e:
logging.exception(e)
error_msg = str(e)
else:
error_msg = None
if args.json:
if error_msg:
print(dumps({'error': error_msg}))
sys.exit(1)
print(onions.to_json())
else:
if error_msg:
logging.error(error_msg)
sys.exit(1)
print(onions)
if __name__ == '__main__':
main()

View file

@ -1,43 +1,64 @@
'This class define a service link' 'This class define a service link'
import logging import logging
import os import os
import pathlib
import re import re
from base64 import b32encode
from hashlib import sha1
from Crypto.PublicKey import RSA from pytor import OnionV3
from pytor.onion import EmptyDirException
class ServicesGroup(object): class ServicesGroup(object):
name = None name = None
_priv_key = None version = None
_key_in_secrets = False imported_key = False
_default_version = 3
_onion = None
_hidden_service_dir = "/var/lib/tor/hidden_service/"
hidden_service_dir = "/var/lib/tor/hidden_service/" def __init__(self,
name=None,
def __init__(self, name=None, service=None, hidden_service_dir=None): service=None,
version=None,
hidden_service_dir=None):
name_regex = r'^[a-zA-Z0-9-_]+$' name_regex = r'^[a-zA-Z0-9-_]+$'
self.hidden_service_dir = hidden_service_dir or self.hidden_service_dir self.onion_map = {
3: OnionV3,
}
if not name and not service: if not name and not service:
raise Exception( raise Exception(
'Init service group with a name or service at least' 'Init service group with a name or service at least'
) )
self.services = [] self.services = []
self.name = name or service.host self.name = name or service.host
if hidden_service_dir:
self._hidden_service_dir = hidden_service_dir
if not re.match(name_regex, self.name): if not re.match(name_regex, self.name):
raise Exception( raise Exception(
'Group {name} has invalid name'.format(name=self.name) 'Group {name} has invalid name'.format(name=self.name)
) )
if service: if service:
self.add_service(service) self.add_service(service)
self.set_version(version or self._default_version)
self.load_key()
if not self._priv_key:
self.gen_key() self.gen_key()
def set_version(self, version):
version = int(version)
if version not in self.onion_map:
raise Exception(
f'Url version {version} is not supported'
)
self.version = version
self._onion = self.onion_map[version]()
@property
def hidden_service_dir(self):
return os.path.join(self._hidden_service_dir, self.name)
def add_service(self, service): def add_service(self, service):
if service not in self.services: if service not in self.services:
if self.get_service_by_host(service.host): if self.get_service_by_host(service.host):
@ -50,15 +71,18 @@ class ServicesGroup(object):
return service return service
def add_key(self, key): def add_key(self, key):
if self._key_in_secrets: if self.imported_key:
logging.warning('Secret key already set, overriding') logging.warning('Secret key already set, overriding')
self._priv_key = key if isinstance(key, str):
self._key_in_secrets = False key = key.encode('ascii')
self._onion.set_private_key(key)
self.imported_key = True
def __iter__(self): def __iter__(self):
yield 'name', self.name yield 'name', self.name
yield 'onion', self.onion_url yield 'onion', self.onion_url
yield 'urls', list(self.urls) yield 'urls', list(self.urls)
yield 'version', self.version
def __str__(self): def __str__(self):
return '{name}: {urls}'.format(name=self.name, return '{name}: {urls}'.format(name=self.name,
@ -66,16 +90,7 @@ class ServicesGroup(object):
@property @property
def onion_url(self): def onion_url(self):
"Get onion url from private key" return self._onion.onion_hostname
# Convert private RSA to public DER
priv = RSA.importKey(self._priv_key.strip())
der = priv.publickey().exportKey("DER")
# hash key, keep first half of sha1, base32 encode
onion = b32encode(sha1(der[22:]).digest()[:10])
return '{onion}.onion'.format(onion=onion.decode().lower())
@property @property
def urls(self): def urls(self):
@ -88,30 +103,18 @@ class ServicesGroup(object):
'Write key on disk and set tor service' 'Write key on disk and set tor service'
if not hidden_service_dir: if not hidden_service_dir:
hidden_service_dir = self.hidden_service_dir hidden_service_dir = self.hidden_service_dir
serv_dir = os.path.join(hidden_service_dir, self.name) if not os.path.isdir(hidden_service_dir):
os.makedirs(serv_dir, exist_ok=True) pathlib.Path(hidden_service_dir).mkdir(parents=True)
os.chmod(serv_dir, 0o700) self._onion.write_hidden_service(hidden_service_dir, force=True)
with open(os.path.join(serv_dir, 'private_key'), 'w') as f:
f.write(self._priv_key)
os.fchmod(f.fileno(), 0o600)
with open(os.path.join(serv_dir, 'hostname'), 'w') as f:
f.write(self.onion_url)
def _load_key(self, key_file): def _load_key(self, key_file):
if os.path.exists(key_file): with open(key_file, 'rb') as f:
with open(key_file, 'r') as f: self._onion.set_private_key_from_file(f)
key = f.read().encode()
if not len(key):
return
try:
rsa = RSA.importKey(key)
self._priv_key = rsa.exportKey("PEM").decode()
except BaseException:
raise('Fail to load key for {name} services'.format(
name=self.name
))
def load_key(self): def load_key(self, override=False, secret=True):
if self.imported_key and not override:
return
if secret:
self.load_key_from_secrets() self.load_key_from_secrets()
self.load_key_from_conf() self.load_key_from_conf()
@ -122,8 +125,9 @@ class ServicesGroup(object):
return return
try: try:
self._load_key(secret_file) self._load_key(secret_file)
self._key_in_secrets = True self.imported_key = True
except BaseException: except BaseException as e:
logging.exception(e)
logging.warning('Fail to load key from secret, ' logging.warning('Fail to load key from secret, '
'check the key or secret name collision') 'check the key or secret name collision')
@ -131,16 +135,21 @@ class ServicesGroup(object):
'Load key from disk if exists' 'Load key from disk if exists'
if not hidden_service_dir: if not hidden_service_dir:
hidden_service_dir = self.hidden_service_dir hidden_service_dir = self.hidden_service_dir
key_file = os.path.join(hidden_service_dir, if not os.path.isdir(hidden_service_dir):
self.name, return
'private_key') try:
self._load_key(key_file) self._onion.load_hidden_service(hidden_service_dir)
self.imported_key = True
except EmptyDirException:
pass
def gen_key(self): def gen_key(self):
'Generate new 1024 bits RSA key for hidden service' self.imported_key = False
self._priv_key = RSA.generate( return self._onion.gen_new_private_key()
bits=1024,
).exportKey("PEM").decode() @property
def _priv_key(self):
return self._onion.get_private_key()
class Ports: class Ports:

1285
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

BIN
private_key_bar_v3 Normal file

Binary file not shown.

BIN
private_key_foo_v3 Normal file

Binary file not shown.

47
pyproject.toml Normal file
View file

@ -0,0 +1,47 @@
[tool.poetry]
name = "docker-tor-hidden-service"
version = "0.7.1"
description = "Display onion sites hosted"
authors = ["Christophe Mehay <cmehay@nospam.student.42.fr>"]
license = "WTFPL"
repository = "https://github.com/cmehay/docker-tor-hidden-service"
classifiers=[
"Programming Language :: Python",
"Development Status :: 1 - Planning",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Topic :: System :: Installation/Setup",
]
packages = [
{ include = "onions" },
]
[tool.poetry.scripts]
onions = "onions:main"
[tool.poetry.dependencies]
python = ">=3.10,<3.11"
Jinja2 = ">=2.10"
importlib_metadata = ">=1.6.0"
vanguards = "^0.3.1"
ipy = ">=1.00"
pytor = '^0.1.9'
pyentrypoint = "^0.8.0"
[tool.poetry.dev-dependencies]
autopep8 = ">=1.5.2"
tox = ">=3.15.0"
cryptography = ">=3.2"
pylint = ">=2.5.2"
ptpython = ">=3.0.2"
black = ">=22.6.0"
pre-commit = "^2.20.0"
pytest = ">=5.4.2"
pyfakefs = ">=4.0.2"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

0
tests/__init__.py Normal file
View file

695
tests/onions_test.py Normal file
View file

@ -0,0 +1,695 @@
import configparser
import json
import os
import re
import pytest
from base64 import b32encode
from base64 import b64decode
from hashlib import sha1
from onions import Onions
def get_key_and_onion(version=3):
key = {}
onion = {}
pub = {}
key[
3
] = """
PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++j96H1X/gq14N
wLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM
"""
pub[
3
] = """
PT0gZWQyNTUxOXYxLXB1YmxpYzogdHlwZTAgPT0AAAC9kzftiea/kb+TWlCEVNpfUJLVk+rFIoMG
m9/hW13isA==
"""
onion[3] = "xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion"
return key[version].strip(), onion[version]
def get_torrc_template():
return r"""
{% for service_group in onion.services %}
HiddenServiceDir {{service_group.hidden_service_dir}}
{% if service_group.version == 3 %}
HiddenServiceVersion 3
{% endif %}
{% for service in service_group.services %}
{% for port in service.ports %}
{% if port.is_socket %}
HiddenServicePort {{port.port_from}} {{port.dest}}
{% endif %}
{% if not port.is_socket %}
HiddenServicePort {{port.port_from}} {{service.host}}:{{port.dest}}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
DataDirectory {{ onion.data_directory }}
{% if 'TOR_SOCKS_PORT' in env %}
SocksPort {{env['TOR_SOCKS_PORT']}}
{% else %}
SocksPort 0
{% endif %}
{% if envtobool('TOR_EXIT_RELAY', False) %}
ExitRelay 1
{% else %}
ExitRelay 0
{% endif %}
{% if onion.enable_control_port %}
{% if onion.control_socket %}
ControlPort {{onion.control_socket}}
{% endif %}
{% if not onion.control_socket %}
{% if onion.control_ip_binding.version() == 4 %}
ControlPort {{onion.control_ip_binding}}:{{ onion.control_port }}
{% endif %}
{% if onion.control_ip_binding.version() == 6 %}
ControlPort [{{onion.control_ip_binding}}]:{{ onion.control_port }}
{% endif %}
{% endif %}
{% if onion.control_hashed_password %}
HashedControlPassword {{ onion.control_hashed_password }}
{% endif %}
{% endif %}
{% if 'TOR_EXTRA_OPTIONS' in env %}
{{env['TOR_EXTRA_OPTIONS']}}
{% endif %}
# useless line for Jinja bug
# useless line for Jinja bug
""".strip()
def test_ports(monkeypatch):
env = {
"SERVICE1_PORTS": "80:80",
"SERVICE2_PORTS": "80:80,81:8000",
"SERVICE3_PORTS": "80:unix://unix.socket",
}
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_env()
assert len(os.environ) == 3
assert len(onion.services) == 3
check = 0
for service_group in onion.services:
assert len(service_group.services) == 1
service = service_group.services[0]
if service.host == "service1":
check += 1
assert len(service.ports) == 1
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 80
assert not service.ports[0].is_socket
if service.host == "service2":
check += 3
assert len(service.ports) == 2
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 80
assert service.ports[1].port_from == 81
assert service.ports[1].dest == 8000
if service.host == "service3":
check += 6
assert len(service.ports) == 1
assert service.ports[0].port_from == 80
assert service.ports[0].dest == "unix://unix.socket"
assert service.ports[0].is_socket
assert check == 10
def test_docker_links(fs, monkeypatch):
env = {
"HOSTNAME": "test_env",
"COMPOSE_SERVICE1_1_PORT": "tcp://172.17.0.2:80",
"COMPOSE_SERVICE1_1_PORT_80_TCP": "tcp://172.17.0.2:80",
"COMPOSE_SERVICE1_1_PORT_80_TCP_ADDR": "172.17.0.2",
"COMPOSE_SERVICE1_1_PORT_80_TCP_PORT": "80",
"COMPOSE_SERVICE1_1_PORT_80_TCP_PROTO": "tcp",
"COMPOSE_SERVICE1_1_PORT_8000_TCP": "tcp://172.17.0.2:8000",
"COMPOSE_SERVICE1_1_PORT_8000_TCP_ADDR": "172.17.0.2",
"COMPOSE_SERVICE1_1_PORT_8000_TCP_PORT": "8000",
"COMPOSE_SERVICE1_1_PORT_8000_TCP_PROTO": "tcp",
"COMPOSE_SERVICE1_1_NAME": "/compose_env_1/compose_service1_1",
"SERVICE1_PORT": "tcp://172.17.0.2:80",
"SERVICE1_PORT_80_TCP": "tcp://172.17.0.2:80",
"SERVICE1_PORT_80_TCP_ADDR": "172.17.0.2",
"SERVICE1_PORT_80_TCP_PORT": "80",
"SERVICE1_PORT_80_TCP_PROTO": "tcp",
"SERVICE1_PORT_8000_TCP": "tcp://172.17.0.2:8000",
"SERVICE1_PORT_8000_TCP_ADDR": "172.17.0.2",
"SERVICE1_PORT_8000_TCP_PORT": "8000",
"SERVICE1_PORT_8000_TCP_PROTO": "tcp",
"SERVICE1_NAME": "/compose_env_1/service1",
"SERVICE1_1_PORT": "tcp://172.17.0.2:80",
"SERVICE1_1_PORT_80_TCP": "tcp://172.17.0.2:80",
"SERVICE1_1_PORT_80_TCP_ADDR": "172.17.0.2",
"SERVICE1_1_PORT_80_TCP_PORT": "80",
"SERVICE1_1_PORT_80_TCP_PROTO": "tcp",
"SERVICE1_1_PORT_8000_TCP": "tcp://172.17.0.2:8000",
"SERVICE1_1_PORT_8000_TCP_ADDR": "172.17.0.2",
"SERVICE1_1_PORT_8000_TCP_PORT": "8000",
"SERVICE1_1_PORT_8000_TCP_PROTO": "tcp",
"SERVICE1_1_NAME": "/compose_env_1/service1_1",
}
etc_host = """
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 service1 bf447f22cdba compose_service1_1
172.17.0.2 service1_1 bf447f22cdba compose_service1_1
172.17.0.2 compose_service1_1 bf447f22cdba
""".strip()
fs.create_file("/etc/hosts", contents=etc_host)
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_links()
assert len(onion.services) == 1
group = onion.services[0]
assert len(group.services) == 1
service = group.services[0]
assert len(service.ports) == 2
assert set((port.port_from, port.dest) for port in service.ports) == set(
[(80, 80), (8000, 8000)]
)
def test_key_v3(monkeypatch):
key, onion_url = get_key_and_onion(version=3)
env = {
"GROUP1_TOR_SERVICE_HOSTS": "80:service1:80,81:service2:80",
"GROUP1_TOR_SERVICE_VERSION": "3",
"GROUP1_TOR_SERVICE_KEY": key,
}
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_env()
onion._load_keys_in_services()
assert len(os.environ) == 3
assert len(onion.services) == 1
assert onion.services[0].onion_url == onion_url
def test_key_in_secret(fs, monkeypatch):
env = {
# "GROUP1_TOR_SERVICE_HOSTS": "80:service1:80",
"GROUP2_TOR_SERVICE_HOSTS": "80:service2:80",
"GROUP3_TOR_SERVICE_HOSTS": "80:service3:80",
"GROUP3_TOR_SERVICE_VERSION": "3",
}
monkeypatch.setattr(os, "environ", env)
# key_v2, onion_url_v2 = get_key_and_onion()
key_v3, onion_url_v3 = get_key_and_onion(version=3)
fs.create_file("/run/secrets/group3", contents=b64decode(key_v3))
onion = Onions()
onion._get_setup_from_env()
onion._load_keys_in_services()
# group1 = onion.find_group_by_name("group1")
group2 = onion.find_group_by_name("group2")
group3 = onion.find_group_by_name("group3")
# assert group1.onion_url == onion_url_v2
assert group2.onion_url != onion_url_v3
assert group3.onion_url == onion_url_v3
def test_configuration(fs, monkeypatch, tmpdir):
extra_options = """
HiddenServiceNonAnonymousMode 1
HiddenServiceSingleHopMode 1
""".strip()
env = {
"SERVICE1_SERVICE_NAME": "group1",
"SERVICE2_SERVICE_NAME": "group1",
"SERVICE3_SERVICE_NAME": "group2",
"SERVICE1_PORTS": "80:80",
"SERVICE2_PORTS": "81:80,82:8000",
"SERVICE3_PORTS": "80:unix://unix.socket",
"GROUP3_TOR_SERVICE_HOSTS": "80:service4:888,81:service5:8080",
"GROUP4_TOR_SERVICE_VERSION": "3",
"GROUP4_TOR_SERVICE_HOSTS": "81:unix://unix2.sock",
"GROUP3V3_TOR_SERVICE_VERSION": "3",
"GROUP3V3_TOR_SERVICE_HOSTS": "80:service4:888,81:service5:8080",
"SERVICE5_TOR_SERVICE_HOSTS": "80:service5:80",
"TOR_EXTRA_OPTIONS": extra_options,
}
hidden_dir = "/var/lib/tor/hidden_service"
monkeypatch.setattr(os, "environ", env)
monkeypatch.setattr(os, "fchmod", lambda x, y: None)
torrc_tpl = get_torrc_template()
fs.create_file("/var/local/tor/torrc.tpl", contents=torrc_tpl)
fs.create_file("/etc/tor/torrc")
fs.create_dir(hidden_dir)
onion = Onions()
onion._get_setup_from_env()
onion._load_keys_in_services()
onion.apply_conf()
onions_urls = {}
for dir in os.listdir(hidden_dir):
with open(os.path.join(hidden_dir, dir, "hostname"), "r") as f:
onions_urls[dir] = f.read().strip()
with open("/etc/tor/torrc", "r") as f:
torrc = f.read()
print(torrc)
assert "HiddenServiceDir /var/lib/tor/hidden_service/group1" in torrc
assert "HiddenServicePort 80 service1:80" in torrc
assert "HiddenServicePort 81 service2:80" in torrc
assert "HiddenServicePort 82 service2:8000" in torrc
assert "HiddenServiceDir /var/lib/tor/hidden_service/group2" in torrc
assert "HiddenServicePort 80 unix://unix.socket" in torrc
assert "HiddenServiceDir /var/lib/tor/hidden_service/group3" in torrc
assert "HiddenServiceDir /var/lib/tor/hidden_service/group4" in torrc
assert "HiddenServiceDir /var/lib/tor/hidden_service/group3v3" in torrc
assert "HiddenServiceDir /var/lib/tor/hidden_service/service5" in torrc
assert torrc.count("HiddenServicePort 80 service4:888") == 2
assert torrc.count("HiddenServicePort 81 service5:8080") == 2
assert torrc.count("HiddenServicePort 80 service5:80") == 1
assert torrc.count("HiddenServicePort 81 unix://unix2.sock") == 1
assert torrc.count("HiddenServiceVersion 3") == 6
assert "HiddenServiceNonAnonymousMode 1\n" in torrc
assert "HiddenServiceSingleHopMode 1\n" in torrc
assert "ControlPort" not in torrc
# Check parser
onion2 = Onions()
onion2.torrc_parser()
assert len(onion2.services) == 6
assert set(
group.name
for group in onion2.services
# ) == set(['group1', 'group2'])
) == set(["group1", "group2", "group3", "group4", "group3v3", "service5"])
for group in onion2.services:
if group.name == "group1":
assert len(group.services) == 2
assert group.version == 3
assert group.onion_url == onions_urls[group.name]
assert set(service.host for service in group.services) == set(
["service1", "service2"]
)
for service in group.services:
if service.host == "service1":
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 80)])
if service.host == "service2":
assert len(service.ports) == 2
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(81, 80), (82, 8000)])
if group.name == "group2":
assert len(group.services) == 1
assert group.version == 3
assert group.onion_url == onions_urls[group.name]
assert set(service.host for service in group.services) == set(
["group2"]
)
service = group.services[0]
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, "unix://unix.socket")])
if group.name in ["group3", "group3v3"]:
assert len(group.services) == 2
assert group.version == 3
assert group.onion_url == onions_urls[group.name]
assert set(service.host for service in group.services) == set(
["service4", "service5"]
)
for service in group.services:
if service.host == "service4":
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 888)])
if service.host == "service5":
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(81, 8080)])
if group.name == "group4":
assert len(group.services) == 1
assert group.version == 3
assert group.onion_url == onions_urls[group.name]
assert set(service.host for service in group.services) == set(
["group4"]
)
for service in group.services:
assert service.host == "group4"
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(81, "unix://unix2.sock")])
if group.name == "service5":
assert len(group.services) == 1
assert group.version == 3
assert group.onion_url == onions_urls[group.name]
assert set(service.host for service in group.services) == set(
["service5"]
)
for service in group.services:
assert service.host == "service5"
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 80)])
# bug with fakefs, test everything in the same function
env = {
"TOR_CONTROL_PORT": "172.0.1.0:7867",
"TOR_CONTROL_PASSWORD": "secret",
}
def mock_hash(self, password):
self.control_hashed_password = "myhashedpassword"
monkeypatch.setattr(os, "environ", env)
monkeypatch.setattr(Onions, "_hash_control_port_password", mock_hash)
onion = Onions()
onion._setup_control_port()
onion.apply_conf()
with open("/etc/tor/torrc", "r") as f:
torrc = f.read()
print(torrc)
assert "ControlPort 172.0.1.0:7867" in torrc
assert f"HashedControlPassword {onion.control_hashed_password}" in torrc
env = {
"TOR_CONTROL_PORT": "unix:/path/to.socket",
}
monkeypatch.setattr(os, "environ", env)
torrc_tpl = get_torrc_template()
onion = Onions()
onion._setup_control_port()
onion.apply_conf()
with open("/etc/tor/torrc", "r") as f:
torrc = f.read()
print(torrc)
assert "ControlPort unix:/path/to.socket" in torrc
def test_groups(monkeypatch):
env = {
"SERVICE1_SERVICE_NAME": "group1",
"SERVICE2_SERVICE_NAME": "group1",
"SERVICE3_SERVICE_NAME": "group2",
"SERVICE1_PORTS": "80:80",
"SERVICE2_PORTS": "81:80,82:8000",
"SERVICE3_PORTS": "80:unix://unix.socket",
}
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_env()
onion_match = r"^[a-z2-7]{56}.onion$"
assert len(os.environ) == 6
assert len(onion.services) == 2
assert set(group.name for group in onion.services) == set(
["group1", "group2"]
)
for group in onion.services:
if group.name == "group1":
assert len(group.services) == 2
assert set(service.host for service in group.services) == set(
["service1", "service2"]
)
if group.name == "group2":
assert len(group.services) == 1
assert set(service.host for service in group.services) == set(
["service3"]
)
assert re.match(onion_match, group.onion_url)
def test_json(monkeypatch):
env = {
"SERVICE1_SERVICE_NAME": "group1",
"SERVICE2_SERVICE_NAME": "group1",
"SERVICE3_SERVICE_NAME": "group2",
"SERVICE1_PORTS": "80:80",
"SERVICE2_PORTS": "81:80,82:8000",
"SERVICE3_PORTS": "80:unix://unix.socket",
}
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_env()
onion.check_services()
jsn = json.loads(onion.to_json())
assert len(jsn) == 2
assert len(jsn["group1"]) == 3
assert len(jsn["group2"]) == 1
def test_output(monkeypatch):
env = {
"SERVICE1_SERVICE_NAME": "group1",
"SERVICE2_SERVICE_NAME": "group1",
"SERVICE3_SERVICE_NAME": "group2",
"SERVICE1_PORTS": "80:80",
"SERVICE2_PORTS": "81:80,82:8000",
"SERVICE3_PORTS": "80:unix://unix.socket",
}
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_env()
for item in ["group1", "group2", ".onion", ","]:
assert item in str(onion)
def test_not_valid_share_port(monkeypatch):
env = {
"SERVICE1_SERVICE_NAME": "group1",
"SERVICE2_SERVICE_NAME": "group1",
"SERVICE3_SERVICE_NAME": "group2",
"SERVICE1_PORTS": "80:80",
"SERVICE2_PORTS": "80:80,82:8000",
"SERVICE3_PORTS": "80:unix://unix.socket",
}
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_env()
with pytest.raises(Exception) as excinfo:
onion.check_services()
assert "Same port for multiple services" in str(excinfo.value)
def test_not_valid_no_services(monkeypatch):
env = {
"SERVICE1_SERVICE_NAME": "group1",
"SERVICE2_SERVICE_NAME": "group1",
"SERVICE3_SERVICE_NAME": "group2",
}
monkeypatch.setattr(os, "environ", env)
onion = Onions()
onion._get_setup_from_env()
with pytest.raises(Exception) as excinfo:
onion.check_services()
assert "has not ports set" in str(excinfo.value)
def get_vanguards_template():
return r"""
## Global options
[Global]
{% if env.get('TOR_CONTROL_PORT', '').startswith('unix:') %}
{% set _, unix_path = env['TOR_CONTROL_PORT'].split(':', 1) %}
{% elif ':' in env.get('TOR_CONTROL_PORT', '') %}
{% set host, port = env['TOR_CONTROL_PORT'].split(':', 1) %}
{% else %}
{% set host = env.get('TOR_CONTROL_PORT') %}
{% endif %}
control_ip = {{ host or '' }}
control_port = {{ port or 9051 }}
control_socket = {{ unix_path or '' }}
control_pass = {{ env.get('TOR_CONTROL_PASSWORD', '') }}
state_file = {{ env.get('VANGUARDS_STATE_FILE', '/run/tor/data/vanguards.state') }}
{% if 'VANGUARDS_EXTRA_OPTIONS' in env %}
{% set extra_conf = ConfigParser().read_string(env['VANGUARDS_EXTRA_OPTIONS']) %}
{% if 'Global' in extra_conf %}
{% for key, val in extra_conf['Global'].items() %}
{{key}} = {{val}}
{% endfor %}
{% set _ = extra_conf.pop('Global') %}
{% endif %}
{{ extra_conf.to_string() }}
{% endif %}
""".strip() # noqa
def test_vanguards_configuration_sock(fs, monkeypatch):
extra_options = """
[Global]
enable_cbtverify = True
loglevel = DEBUG
[Rendguard]
rend_use_max_use_to_bw_ratio = 4.0
""".strip()
env = {
"TOR_ENABLE_VANGUARDS": "true",
"TOR_CONTROL_PORT": "unix:/path/to/sock",
"VANGUARDS_EXTRA_OPTIONS": extra_options,
}
monkeypatch.setattr(os, "environ", env)
monkeypatch.setattr(os, "fchmod", lambda x, y: None)
torrc_tpl = get_vanguards_template()
fs.create_file("/var/local/tor/vanguards.conf.tpl", contents=torrc_tpl)
fs.create_file("/etc/tor/vanguards.conf")
onion = Onions()
onion.resolve_control_port()
onion._setup_vanguards()
onion._write_vanguards_conf()
vanguard_conf = configparser.ConfigParser()
with open("/etc/tor/vanguards.conf", "r") as f:
print(f.read())
vanguard_conf.read("/etc/tor/vanguards.conf")
assert vanguard_conf["Global"]
assert not vanguard_conf["Global"]["control_ip"]
assert vanguard_conf["Global"]["control_port"] == "9051"
assert vanguard_conf["Global"]["control_socket"] == "/path/to/sock"
assert not vanguard_conf["Global"]["control_pass"]
assert (
vanguard_conf["Global"]["state_file"]
== "/run/tor/data/vanguards.state"
)
assert vanguard_conf["Global"]["enable_cbtverify"]
assert vanguard_conf["Global"]["loglevel"] == "DEBUG"
assert vanguard_conf["Rendguard"]["rend_use_max_use_to_bw_ratio"] == "4.0"
def test_vanguards_configuration_ip(fs, monkeypatch):
env = {
"TOR_ENABLE_VANGUARDS": "true",
"TOR_CONTROL_PORT": "127.0.0.1:7864",
"TOR_CONTROL_PASSWORD": "secret",
}
monkeypatch.setattr(os, "environ", env)
monkeypatch.setattr(os, "fchmod", lambda x, y: None)
torrc_tpl = get_vanguards_template()
fs.create_file("/var/local/tor/vanguards.conf.tpl", contents=torrc_tpl)
fs.create_file("/etc/tor/vanguards.conf")
onion = Onions()
onion.resolve_control_port()
onion._setup_vanguards()
onion._write_vanguards_conf()
vanguard_conf = configparser.ConfigParser()
with open("/etc/tor/vanguards.conf", "r") as f:
print(f.read())
vanguard_conf.read("/etc/tor/vanguards.conf")
assert vanguard_conf["Global"]
assert vanguard_conf["Global"]["control_ip"] == "127.0.0.1"
assert vanguard_conf["Global"]["control_port"] == "7864"
assert not vanguard_conf["Global"]["control_socket"]
assert vanguard_conf["Global"]["control_pass"] == "secret"
assert (
vanguard_conf["Global"]["state_file"]
== "/run/tor/data/vanguards.state"
)

15
tox.ini
View file

@ -1,12 +1,9 @@
[tox] [tox]
envlist = py34, py35, py36 isolated_build = true
changedir=assets/onions/ envlist = py310
setupdir=assets/onions/
skip_missing_interpreters = true
[testenv] [testenv]
deps= whitelist_externals = poetry
pytest commands =
pyfakefs poetry install -v
pytest-mock poetry run pytest tests/
commands=pytest -v