First implementation of transparent client tunnel using SAM
The transparent proxy creates a client tunnel to the requested destination for each client connection. This is untested for now, but a server tunnel is also incoming if this works well. #4 - Investigate extending pr0xy to use SAM
This commit is contained in:
@@ -36,8 +36,9 @@ echo "nameserver 127.0.0.1" > /etc/resolv.conf
|
||||
ulogd -d
|
||||
tcpdump -i any -w /mount/tcp.dmp &
|
||||
|
||||
python3 /opt/bin/fake-dns.py -s "/tmp/fake-dns" &
|
||||
python3 /opt/pr0cks/pr0cks.py \
|
||||
--port ${LOCAL_PROXY_PORT}
|
||||
--proxy "http:${I2PD_IP}:${I2PD_PORT}" \
|
||||
export PYTHONPATH=/opt/bin
|
||||
python3 /opt/bin/trans_proxy/cli.py \
|
||||
--port ${LOCAL_PROXY_PORT} \
|
||||
--sam-host "${I2PD_IP}" \
|
||||
--sam-port "${I2PD_PORT}" \
|
||||
--verbose
|
||||
|
@@ -1,279 +0,0 @@
|
||||
# i2p-docker-proxy
|
||||
# Copyright (C) 2019 LoveIsGrief
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socketserver
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from ipaddress import ip_address, IPv4Address, AddressValueError
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from time import sleep
|
||||
|
||||
from dnslib import dns
|
||||
from dnslib.server import DNSServer, BaseResolver
|
||||
from faker import Faker
|
||||
|
||||
SERIAL_NO = int((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds())
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s: %(message)s', datefmt='%H:%M:%S'))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def validate_fqdn(domain_name):
|
||||
"""
|
||||
Taken from https://stackoverflow.com/questions/2532053/validate-a-hostname-string/33715765#33715765
|
||||
|
||||
:param domain_name:
|
||||
:type domain_name: basestring
|
||||
:rtype: bool
|
||||
"""
|
||||
if domain_name.endswith('.'):
|
||||
domain_name = domain_name[:-1]
|
||||
if len(domain_name) < 1 or len(domain_name) > 253:
|
||||
return False
|
||||
ldh_re = re.compile('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$',
|
||||
re.IGNORECASE)
|
||||
return all(ldh_re.match(x) for x in domain_name.split('.'))
|
||||
|
||||
|
||||
class UnixSocketServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
|
||||
"""
|
||||
A simple, threaded server that follows the DNSServer signatures
|
||||
"""
|
||||
|
||||
def __init__(self, socket_path, request_handler_class, bind_and_activate=True):
|
||||
self._socket_path = socket_path
|
||||
# Remove old socket if it's not being used
|
||||
self._rm_socket()
|
||||
super().__init__(socket_path, request_handler_class, bind_and_activate)
|
||||
self.thread = threading.Thread(target=self.serve_forever)
|
||||
self.thread.daemon = True
|
||||
|
||||
def start_thread(self):
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.shutdown()
|
||||
self.server_close()
|
||||
# Attempt to remove socket before exiting
|
||||
self._rm_socket(True)
|
||||
|
||||
def _rm_socket(self, log_error=False):
|
||||
try:
|
||||
os.unlink(self._socket_path)
|
||||
except Exception as e:
|
||||
if log_error:
|
||||
logging.getLogger("UnixSocketServer").warn(
|
||||
"Couldn't remove unix socket: %s", e
|
||||
)
|
||||
|
||||
def isAlive(self):
|
||||
return self.thread.isAlive()
|
||||
|
||||
|
||||
class UnixSocketHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
host = ""
|
||||
try:
|
||||
# An ip address string is max 16 byes
|
||||
ip_str = self.request.recv(256).strip().decode()
|
||||
ip_address(ip_str)
|
||||
host = RandomResolver.CACHE_IP[ip_str]
|
||||
except AddressValueError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.getLogger("UnixSocketHandler") \
|
||||
.warn("Couldn't handle unix data: %s", e)
|
||||
try:
|
||||
self.request.sendall(bytes(host.encode()))
|
||||
except Exception as e:
|
||||
logging.getLogger("UnixSocketHandler") \
|
||||
.warn("Couldn't respond: %s", e)
|
||||
|
||||
|
||||
class RandomResolver(BaseResolver):
|
||||
"""
|
||||
Resolves a random, public IP for every unique DNS request.
|
||||
"""
|
||||
CACHE_IP = {}
|
||||
CACHE_FQDN = {}
|
||||
_FAKER = Faker(providers=["faker.providers.internet"])
|
||||
_FAKER_LOCK = RLock()
|
||||
|
||||
def __init__(self, resolve_dir):
|
||||
self.resolve_dir = Path(resolve_dir)
|
||||
|
||||
@classmethod
|
||||
def load_dir(cls, resolve_dir):
|
||||
# TODO: implement this
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def ip2path(address, resolve_dir):
|
||||
"""
|
||||
Constructs the path to get stored FQDN of the given IP address
|
||||
|
||||
:param address: IPv4 address
|
||||
:type address: basestring
|
||||
:param resolve_dir: The root dir where the IPs and FQDNs will be stored
|
||||
:type resolve_dir: Path
|
||||
:rtype: Path | None
|
||||
"""
|
||||
try:
|
||||
if not isinstance(ip_address(address), IPv4Address):
|
||||
return
|
||||
except ValueError:
|
||||
return
|
||||
a, b, c, d = address.split(".")
|
||||
return resolve_dir / a / b / c / d
|
||||
|
||||
@classmethod
|
||||
def reload_ip(cls, ip, resolve_dir):
|
||||
"""
|
||||
Retrieves the stored FQDN from the given IP
|
||||
|
||||
:param ip: Should be a valid IPv4 address
|
||||
:type ip: basestring
|
||||
:param resolve_dir:
|
||||
:type resolve_dir:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
ip_path = cls.ip2path(ip, resolve_dir)
|
||||
if not (ip_path and ip_path.is_file()):
|
||||
return
|
||||
fqdn = ip_path.read_text().strip()
|
||||
if not validate_fqdn(fqdn):
|
||||
return
|
||||
|
||||
# Make sure to remove old entries
|
||||
for _ip, _fqdn in cls.CACHE_IP.items():
|
||||
if _fqdn == fqdn:
|
||||
del cls.CACHE_FQDN[_fqdn]
|
||||
break
|
||||
|
||||
cls.CACHE_IP[ip] = fqdn
|
||||
cls.CACHE_FQDN[fqdn] = ip
|
||||
|
||||
@classmethod
|
||||
def create_fake_entry(cls, fqdn):
|
||||
ip = cls._FAKER.ipv4_public(network=False, address_class=None)
|
||||
cls.CACHE_FQDN[fqdn] = ip
|
||||
cls.CACHE_IP[ip] = fqdn
|
||||
logging.getLogger("RandomResolver").info(
|
||||
"New IP for %s: %s",
|
||||
fqdn, ip
|
||||
)
|
||||
return ip
|
||||
|
||||
@classmethod
|
||||
def get_ip(cls, fqdn):
|
||||
fqdn = fqdn.strip('.')
|
||||
with cls._FAKER_LOCK:
|
||||
ip = cls.CACHE_FQDN.get(fqdn)
|
||||
if not ip:
|
||||
ip = cls.create_fake_entry(fqdn)
|
||||
return ip
|
||||
|
||||
def resolve(self, request, handler):
|
||||
"""
|
||||
Resolves a random, public IP for each unique DNS request
|
||||
|
||||
:type request: dnslib.dns.DNSRecord
|
||||
:type handler: dnslib.server.DNSHandler
|
||||
:rtype: dnslib.dns.DNSRecord
|
||||
"""
|
||||
reply = request.reply()
|
||||
fqdn = str(request.q.get_qname())
|
||||
reply.add_answer(dns.RR(
|
||||
fqdn, rdata=dns.A(self.get_ip(fqdn))
|
||||
))
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
def main(args):
|
||||
"""
|
||||
:param args:
|
||||
:type args: argparse.Namespace
|
||||
"""
|
||||
log = logging.getLogger("fake-dns.main")
|
||||
|
||||
port = args.port
|
||||
resolve_dir = Path(args.resolve_dir)
|
||||
|
||||
resolver = RandomResolver(resolve_dir)
|
||||
|
||||
udp_server = DNSServer(resolver, port=port)
|
||||
tcp_server = DNSServer(resolver, port=port, tcp=True)
|
||||
servers = [udp_server, tcp_server]
|
||||
if args.socket_path:
|
||||
socket_path = Path(args.socket_path)
|
||||
socket_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
log.info("Creating unix socket to %s", socket_path)
|
||||
servers.append(UnixSocketServer(str(socket_path), UnixSocketHandler))
|
||||
|
||||
def stop_servers():
|
||||
for _server in servers:
|
||||
_server.stop()
|
||||
|
||||
def handle_sig(signum, frame):
|
||||
logger.info('pid=%d, got signal: %s, stopping...', os.getpid(), signal.Signals(signum).name)
|
||||
stop_servers()
|
||||
exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM | signal.SIGINT, handle_sig)
|
||||
|
||||
logger.info('starting DNS server on port %d', port)
|
||||
for server in servers:
|
||||
server.start_thread()
|
||||
|
||||
try:
|
||||
while udp_server.isAlive():
|
||||
sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
stop_servers()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="A DNS server that returns fake IPs for requests"
|
||||
)
|
||||
parser.add_argument("-p", "--port",
|
||||
help="Which port to use to accept DNS requests",
|
||||
type=int,
|
||||
default=53)
|
||||
parser.add_argument("-r", "--resolve-dir",
|
||||
help="Where the resolves will stored",
|
||||
default="/etc/resolve/")
|
||||
parser.add_argument("-s", "--socket-path",
|
||||
help="A path to create a socket where"
|
||||
" reverse requests can be mad")
|
||||
|
||||
main(parser.parse_args())
|
15
contained/bin/trans_proxy/__init__.py
Normal file
15
contained/bin/trans_proxy/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# i2p-docker-proxy
|
||||
# Copyright (C) 2019 LoveIsGrief
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
128
contained/bin/trans_proxy/cli.py
Normal file
128
contained/bin/trans_proxy/cli.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# i2p-docker-proxy
|
||||
# Copyright (C) 2019 LoveIsGrief
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import typing
|
||||
from time import sleep
|
||||
|
||||
|
||||
from i2plib import sam
|
||||
|
||||
from trans_proxy import fake_dns
|
||||
from trans_proxy.process import AsyncProcess
|
||||
from trans_proxy.servers import ClientTcpTunnel
|
||||
|
||||
ENV_SAM_HOST = "I2P_SAM_HOST"
|
||||
ENV_SAM_PORT = "I2P_SAM_PORT"
|
||||
ENV_DNS_PORT = "I2P_DNS_PORT"
|
||||
|
||||
logger = logging.getLogger("trans_proxy")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Forwards packets to an I2P instance and handles DNS requests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
action='store_true',
|
||||
help="Activates verbose logs")
|
||||
parser.add_argument(
|
||||
"-p", "--port",
|
||||
default=1234,
|
||||
help="Where all traffic should enter to be forwarded")
|
||||
parser.add_argument(
|
||||
"--sam-host",
|
||||
default=os.environ.get(ENV_SAM_HOST, sam.DEFAULT_ADDRESS[0]))
|
||||
parser.add_argument(
|
||||
"--sam-port",
|
||||
default=os.environ.get(ENV_SAM_PORT, sam.DEFAULT_ADDRESS[1]))
|
||||
|
||||
dns_group = parser.add_argument_group('dns')
|
||||
dns_group.add_argument(
|
||||
"--dns-port",
|
||||
default=os.environ.get(ENV_DNS_PORT, 1053))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
||||
|
||||
with multiprocessing.Manager() as m:
|
||||
ip_dict = m.dict()
|
||||
processes = [
|
||||
AsyncProcess(
|
||||
target=start_client_tcp_tunnel,
|
||||
kwargs={
|
||||
"sam_host": args.sam_host,
|
||||
"sam_port": args.sam_port,
|
||||
"ip_dict": ip_dict,
|
||||
}
|
||||
),
|
||||
multiprocessing.Process(
|
||||
target=fake_dns.main,
|
||||
kwargs={
|
||||
"port": args.dns_port,
|
||||
}
|
||||
)
|
||||
]
|
||||
exec_processes(processes)
|
||||
|
||||
|
||||
def exec_processes(processes: typing.List[multiprocessing.Process]):
|
||||
"""
|
||||
Starts processes and waits for them to end
|
||||
"""
|
||||
# TODO: manage processes better
|
||||
for process in processes:
|
||||
process.start()
|
||||
|
||||
try:
|
||||
all_processes_done = False
|
||||
while not all_processes_done:
|
||||
sleep(1)
|
||||
all_processes_done = all(
|
||||
not process.is_alive()
|
||||
for process in processes
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
for process in processes:
|
||||
if process.is_alive():
|
||||
process.close()
|
||||
|
||||
|
||||
async def start_client_tcp_tunnel(
|
||||
sam_host,
|
||||
sam_port,
|
||||
ip_dict,
|
||||
host="127.0.0.1", port=1234,
|
||||
**kwargs
|
||||
):
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(lambda: ClientTcpTunnel(
|
||||
sam_host=sam_host,
|
||||
sam_port=sam_port,
|
||||
ip_dict=ip_dict,
|
||||
), host=host, port=port)
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
156
contained/bin/trans_proxy/fake_dns.py
Normal file
156
contained/bin/trans_proxy/fake_dns.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# i2p-docker-proxy
|
||||
# Copyright (C) 2019 LoveIsGrief
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from time import sleep
|
||||
|
||||
from dnslib import dns
|
||||
from dnslib.server import DNSServer, BaseResolver
|
||||
from faker import Faker
|
||||
|
||||
from trans_proxy.utils import ReverseDict
|
||||
|
||||
SERIAL_NO = int((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds())
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s: %(message)s', datefmt='%H:%M:%S'))
|
||||
|
||||
logger = logging.getLogger("fake_dns")
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def validate_fqdn(domain_name):
|
||||
"""
|
||||
Taken from https://stackoverflow.com/questions/2532053/validate-a-hostname-string/33715765#33715765
|
||||
|
||||
:param domain_name:
|
||||
:type domain_name: basestring
|
||||
:rtype: bool
|
||||
"""
|
||||
if domain_name.endswith('.'):
|
||||
domain_name = domain_name[:-1]
|
||||
if len(domain_name) < 1 or len(domain_name) > 253:
|
||||
return False
|
||||
ldh_re = re.compile('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$',
|
||||
re.IGNORECASE)
|
||||
return all(ldh_re.match(x) for x in domain_name.split('.'))
|
||||
|
||||
|
||||
class RandomResolver(BaseResolver):
|
||||
"""
|
||||
Resolves a random, public IP for every unique DNS request.
|
||||
"""
|
||||
_FAKER = Faker(providers=["faker.providers.internet"])
|
||||
_FAKER_LOCK = RLock()
|
||||
|
||||
def __init__(self, ip_dict=None):
|
||||
if dict is None:
|
||||
self.fqdn_dict = {}
|
||||
else:
|
||||
self.fqdn_dict = ReverseDict(ip_dict)
|
||||
|
||||
def create_fake_entry(self, fqdn):
|
||||
ip = self._FAKER.ipv4_public(network=False, address_class=None)
|
||||
self.fqdn_dict[fqdn] = ip
|
||||
logging.getLogger("RandomResolver").info(
|
||||
"New IP for %s: %s",
|
||||
fqdn, ip
|
||||
)
|
||||
return ip
|
||||
|
||||
def get_ip(self, fqdn):
|
||||
fqdn = fqdn.strip('.')
|
||||
with self._FAKER_LOCK:
|
||||
ip = self.fqdn_dict.get(fqdn)
|
||||
if not ip:
|
||||
ip = self.create_fake_entry(fqdn)
|
||||
return ip
|
||||
|
||||
def resolve(self, request, handler):
|
||||
"""
|
||||
Resolves a random, public IP for each unique DNS request
|
||||
|
||||
:type request: dnslib.dns.DNSRecord
|
||||
:type handler: dnslib.server.DNSHandler
|
||||
:rtype: dnslib.dns.DNSRecord
|
||||
"""
|
||||
reply = request.reply()
|
||||
fqdn = str(request.q.get_qname())
|
||||
reply.add_answer(dns.RR(
|
||||
fqdn, rdata=dns.A(self.get_ip(fqdn))
|
||||
))
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
def main(port: int = 53, ip_dict=None):
|
||||
"""
|
||||
Creates a UDP and TCP DNS server with a FakeResolver
|
||||
|
||||
:param port: On localhost to access DNS requests
|
||||
:param ip_dict: A dict into which IP addresses of resolved domains will be stored
|
||||
"""
|
||||
ip_dict = ip_dict or {}
|
||||
resolver = RandomResolver(ip_dict)
|
||||
|
||||
udp_server = DNSServer(resolver, port=port)
|
||||
tcp_server = DNSServer(resolver, port=port, tcp=True)
|
||||
servers = [udp_server, tcp_server]
|
||||
|
||||
def stop_servers():
|
||||
for _server in servers:
|
||||
_server.stop()
|
||||
|
||||
def handle_sig(signum, frame):
|
||||
logger.info('pid=%d, got signal: %s, stopping...', os.getpid(), signal.Signals(signum).name)
|
||||
stop_servers()
|
||||
exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM | signal.SIGINT, handle_sig)
|
||||
|
||||
logger.info('starting DNS server on port %d', port)
|
||||
for server in servers:
|
||||
server.start_thread()
|
||||
|
||||
try:
|
||||
while udp_server.isAlive():
|
||||
sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
stop_servers()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="A DNS server that returns fake IPs for requests"
|
||||
)
|
||||
parser.add_argument("-p", "--port",
|
||||
help="Which port to use to accept DNS requests",
|
||||
type=int,
|
||||
default=53)
|
||||
|
||||
main(**parser.parse_args().__dict__)
|
30
contained/bin/trans_proxy/process.py
Normal file
30
contained/bin/trans_proxy/process.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# i2p-docker-proxy
|
||||
# Copyright (C) 2019 LoveIsGrief
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
from inspect import isawaitable
|
||||
|
||||
|
||||
class AsyncProcess(multiprocessing.Process):
|
||||
"""
|
||||
Adds the ability to run async targets in new processes
|
||||
"""
|
||||
|
||||
def run(self) -> None:
|
||||
if self._target:
|
||||
possible_awaitable = self._target(*self._args, **self._kwargs)
|
||||
if isawaitable(possible_awaitable):
|
||||
asyncio.run(possible_awaitable)
|
80
contained/bin/trans_proxy/servers.py
Normal file
80
contained/bin/trans_proxy/servers.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# i2p-docker-proxy
|
||||
# Copyright (C) 2019 LoveIsGrief
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import asyncio
|
||||
import typing
|
||||
from asyncio import transports
|
||||
from asyncio.streams import StreamReader, StreamWriter
|
||||
|
||||
import i2plib as i2plib
|
||||
|
||||
from trans_proxy.utils import get_original_ip
|
||||
|
||||
|
||||
class ClientTcpTunnel(asyncio.Protocol):
|
||||
"""
|
||||
The transparent proxy that forwards clients to their I2P destinations
|
||||
"""
|
||||
|
||||
def __init__(self, sam_host: str, sam_port: int, ip_dict: dict):
|
||||
self.sam_port = sam_port
|
||||
self.sam_host = sam_host
|
||||
self.ip_dict = ip_dict
|
||||
|
||||
self.reader: typing.Optional[StreamReader] = None
|
||||
self.writer: typing.Optional[StreamWriter] = None
|
||||
self.transport: typing.Optional[transports.Transport] = None
|
||||
# TODO generate a unique session name
|
||||
self.session_name = "test-connect"
|
||||
self._destination = None
|
||||
|
||||
@property
|
||||
def destination(self) -> str:
|
||||
"""
|
||||
The I2P destination that this tunnel is pointing to
|
||||
"""
|
||||
if self._destination is None:
|
||||
self._update_destination()
|
||||
return self._destination
|
||||
|
||||
def _update_destination(self):
|
||||
# TODO: handle non-existent destination
|
||||
self._destination = self.ip_dict[
|
||||
get_original_ip(self.transport.get_extra_info('socket'))
|
||||
]
|
||||
|
||||
async def connection_made(self, transport: transports.Transport) -> None:
|
||||
self.transport = transport
|
||||
self._update_destination()
|
||||
session_name = self.session_name
|
||||
|
||||
# create a SAM stream session
|
||||
await i2plib.create_session(session_name, (self.sam_host, self.sam_port))
|
||||
|
||||
# connect to a destination
|
||||
# TODO: Add destination_lookup to FakeResolver
|
||||
self.reader, self.writer = await i2plib.stream_connect(session_name, self.destination)
|
||||
|
||||
async def data_received(self, data: bytes) -> None:
|
||||
# write data to a socket
|
||||
self.writer.write(data)
|
||||
|
||||
# asynchronously receive data
|
||||
while i2p_response := await self.reader.read(4096):
|
||||
self.transport.write(i2p_response)
|
||||
|
||||
def eof_received(self):
|
||||
# close the connection
|
||||
self.writer.close()
|
53
contained/bin/trans_proxy/utils.py
Normal file
53
contained/bin/trans_proxy/utils.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# i2p-docker-proxy
|
||||
# Copyright (C) 2019 LoveIsGrief
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import socket
|
||||
import struct
|
||||
|
||||
SO_ORIGINAL_DST = 80
|
||||
"""
|
||||
A socket option set by netfilter when packets are redirected in the NAT table
|
||||
"""
|
||||
|
||||
|
||||
class ReverseDict(dict):
|
||||
"""
|
||||
A dict that stores the reverse of itself in another dict
|
||||
"""
|
||||
|
||||
def __init__(self, reverse_dict: dict):
|
||||
super().__init__()
|
||||
self._reverse_dict = reverse_dict
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super(ReverseDict, self).__setitem__(key, value)
|
||||
self._reverse_dict[value] = key
|
||||
|
||||
def update(self, other=None, **kvps) -> None:
|
||||
super(ReverseDict, self).update(other, **kvps)
|
||||
self._reverse_dict.update({
|
||||
v: k
|
||||
for k, v in self.items()
|
||||
})
|
||||
|
||||
def clear(self) -> None:
|
||||
super(ReverseDict, self).clear()
|
||||
self._reverse_dict.clear()
|
||||
|
||||
|
||||
def get_original_ip(sock: socket.socket) -> str:
|
||||
odestdata = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16)
|
||||
_, port, a1, a2, a3, a4 = struct.unpack("!HHBBBBxxxxxxxx", odestdata)
|
||||
return "%d.%d.%d.%d" % (a1, a2, a3, a4)
|
Reference in New Issue
Block a user