diff --git a/README.md b/README.md index e1b9e528f1e181fecf4bfaae538b4e2c99851933..71be9f61252c7a352e3d64805b70f18e461b5840 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Install dependances: Install the following packages: `libleveldb-dev` `libsodium-dev` `python3` `python3-pip` - sudo pip3 install libnacl duniterpy silkaj py-ubjson plyvel + sudo pip3 install libnacl duniterpy silkaj py-ubjson plyvel PySocks + python3 --version # If Python < 3.6: sudo pip3 install python2_secrets @@ -27,14 +28,36 @@ servers with different configs) (note that each server must have different dir) python3 server.py -i -d ~/.gmixer-g1 -Edit config and set your values. If `salt` or `password` are empty, they will be asked -at runtime. For security reasons, it is not a good idea to write them in commandline. +Edit config and set your values. If you are not using any proxy, `Bind_` and `Public_` addresses should be the same. +If `salt` or `password` are empty, they will be asked at runtime. For security reasons, it is not a good idea to write them in commandline. Now, start the server: python3 server.py -s # Use -d if using a different dir than default +### Proxy + +To use a SOCKS5 proxy for client connections, set the `Proxy` value in `config.ini` as following: + + Proxy: 127.0.0.1 8080 # address port + Proxy_OnionOnly: 0 + +To use a proxy only when connecting to a `.onion` address, set the `Proxy_OnionOnly` value to `1`: + + + Proxy: 127.0.0.1 9050 + Proxy_OnionOnly: 1 + +If using a reverse proxy (i.e. Tor hidden service), example: + + Bind_Host: 127.0.0.1 # local address set in torrc file + Bind_Port: 10951 + Public_Host: svetsae7j3usrycn.onion # other nodes contact me with this address + Public_Port: 10951 + +To unactivate proxy, leave empty or remove the `Proxy` line. + ## Protocols ### Peer list file diff --git a/client.py b/client.py index 730cf4114978bd1dd58792b115538abd81b57bdf..d24ec3e8c2de0785030f6cf05a431966c823a062 100644 --- a/client.py +++ b/client.py @@ -48,8 +48,8 @@ class Confirmation(): def genKeys(): return secrets.token_urlsafe(), secrets.token_urlsafe() -def get_peers(host): - header, content = sdata(host, "GET", "/pubkey/list") +def get_peers(host, proxy=None, proxy_onion_only=False): + header, content = sdata(host, "GET", "/pubkey/list", proxy=proxy, proxy_onion_only=proxy_onion_only) try: data = ubjson.loadb(content) @@ -86,7 +86,7 @@ def build_path(peers, receiver, layers=3): path.append(receiver) return host, path -def mix(amount, base, sender, path, host): +def mix(amount, base, sender, path, host, proxy=None, proxy_onion_only=False): start_time = time.time() onetime_keys = [] @@ -104,7 +104,7 @@ def mix(amount, base, sender, path, host): while True: try: - header, content = sdata(host, "POST", "/pubkey/mix/"+sender.pubkey+"/"+str(int(amount))+"/"+str(int(base))+"/client", message) + header, content = sdata(host, "POST", "/pubkey/mix/"+sender.pubkey+"/"+str(int(amount))+"/"+str(int(base))+"/client", message, proxy=proxy, proxy_onion_only=proxy_onion_only) except ConnectionRefusedError: print("Error: Connection refused; retrying...") time.sleep(3) @@ -125,7 +125,7 @@ def mix(amount, base, sender, path, host): time.sleep(5) print("Asking input node for confirmation...") try: - header, content = sdata(host, "GET", "/getconfirm/"+sender.pubkey+"/"+out_comment) + header, content = sdata(host, "GET", "/getconfirm/"+sender.pubkey+"/"+out_comment, proxy=proxy, proxy_onion_only=proxy_onion_only) except ConnectionRefusedError: continue @@ -202,7 +202,7 @@ def mix(amount, base, sender, path, host): exit() -async def test1(host, receiver, amount=1000, layers=3): +async def test1(host, receiver, amount=1000, layers=3, proxy=None, proxy_onion_only=False): salt = getpass.getpass("Salt: ") password = getpass.getpass("Psw: ") keys = SigningKey.from_credentials(salt, password) # sender @@ -210,7 +210,7 @@ async def test1(host, receiver, amount=1000, layers=3): if input("OK? [yn]: ").lower() != "y": return - peers = get_peers(host) + peers = get_peers(host, proxy, proxy_onion_only) if peers == None: print("Error getting peer list") exit(1) @@ -224,7 +224,7 @@ async def test1(host, receiver, amount=1000, layers=3): if input("OK? [yn]: ").lower() == "y": break - mix(amount, 0, keys, path, host1) + mix(amount, 0, keys, path, host1, proxy, proxy_onion_only) if __name__ == "__main__": if "--help" in sys.argv: @@ -235,9 +235,11 @@ Options: -l <int> number of onion layers (default 3) -a <int> amount (default 1000 = 10.00Ğ1) -h <host> <port> host for sync peer list + -p <host> <port> SOCKS5 proxy + --onion use proxy only when connecting to .onion Example: -python3 client.py -h 2a01:cb19:982:f800:ec34:6087:5197:4752 10951 -r 78ZwwgpgdH5uLZLbThUQH7LKwPgjMunYfLiCfUCySkM8 +python3 client.py -h 127.0.0.1 10951 -r 78ZwwgpgdH5uLZLbThUQH7LKwPgjMunYfLiCfUCySkM8 -p 127.0.0.1 9050 --onion ⤷ send 10Ğ1 to Duniter developers """) exit() @@ -253,5 +255,9 @@ python3 client.py -h 2a01:cb19:982:f800:ec34:6087:5197:4752 10951 -r 78ZwwgpgdH5 port = int(getargv("-h", "", 2)) amount = int(getargv("-a", "1000")) layers = int(getargv("-l", "3")) + proxy = None + if "-p" in sys.argv: + proxy = (getargv("-p", "127.0.0.1", 1), int(getargv("-p", 9050, 2))) + proxy_onion_only = "--onion" in sys.argv - asyncio.get_event_loop().run_until_complete(test1((host, port), receiver, amount, layers)) + asyncio.get_event_loop().run_until_complete(test1((host, port), receiver, amount, layers, proxy, proxy_onion_only)) diff --git a/server.py b/server.py index 275952ec539e37bfcb422607eec49ace77eaa004..62b3b0dce6c403b0eb9b918f3e0e5a571a3d77d3 100644 --- a/server.py +++ b/server.py @@ -38,8 +38,10 @@ out_ = something I'm sending to a node """ DIR = "~/.gmixer" -HOST = socket.gethostname() -PORT = "10951" +BIND_HOST = socket.gethostname() +BIND_PORT = "10951" +PUBLIC_HOST = BIND_HOST +PUBLIC_PORT = BIND_PORT BMA_HOSTS = "g1.duniter.fr 443,g1.duniter.org 443,g1.presles.fr 443,g1.cgeek.fr 443,ts.g1.librelois.fr 443" ID_SALT = "" # if empty, will be asked at runtime ID_PSW = "" # if empty, will be asked at runtime @@ -79,7 +81,7 @@ class TX: self.confirms = b"" self.tx_sent = False - def genMixConfirm(self, keys, conf): + def genMixConfirm(self, keys): message = { "document": "gmixer-mixconfirm1", "sender_pubkey": self.sender_pubkey, @@ -161,12 +163,20 @@ def readConfig(dir): conf["Crypto"] = {} if not "Mix" in conf: conf["Mix"] = {} - if not "Host" in conf["Server"]: - conf["Server"]["Host"] = HOST - if not "Port" in conf["Server"]: - conf["Server"]["Port"] = PORT + if not "Bind_Host" in conf["Server"]: + conf["Server"]["Bind_Host"] = BIND_HOST + if not "Bind_Port" in conf["Server"]: + conf["Server"]["Bind_Port"] = BIND_PORT + if not "Public_Host" in conf["Server"]: + conf["Server"]["Public_Host"] = PUBLIC_HOST + if not "Public_Port" in conf["Server"]: + conf["Server"]["Public_Port"] = PUBLIC_PORT if not "BMA_Hosts" in conf["Client"]: conf["Client"]["BMA_Hosts"] = BMA_HOSTS + if not "Proxy" in conf["Client"]: + conf["Client"]["Proxy"] = "" + if not "Proxy_OnionOnly" in conf["Client"]: + conf["Client"]["Proxy_OnionOnly"] = "0" if not "ID_Salt" in conf["Crypto"]: conf["Crypto"]["ID_Salt"] = ID_SALT if not "ID_Password" in conf["Crypto"]: @@ -200,7 +210,7 @@ class ServerThread(Thread): self.work = True def run(self): - server_addr = (self.conf["Server"]["Host"], int(self.conf["Server"]["Port"])) + server_addr = (self.conf["Server"]["Bind_Host"], int(self.conf["Server"]["Bind_Port"])) if ":" in server_addr[0]: # IPv6 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: # IPv4 @@ -326,7 +336,7 @@ class ServerThread(Thread): # Save tx in pool t = time.time() - tx = TX(sender_pubkey, receiver_pubkey, onetime_pubkey, in_amount, in_base, in_amount, in_base, message, in_comment, out_comment, send_confirm, t, t+self.conf["Mix"]["MixReqAgeMax"]) + tx = TX(sender_pubkey, receiver_pubkey, onetime_pubkey, in_amount, in_base, in_amount, in_base, message, in_comment, out_comment, send_confirm, t, t+int(self.conf["Mix"]["MixReqAgeMax"])) last_node = len(message) == 0 tx.need_send = not last_node tx.can_confirm = last_node @@ -467,6 +477,11 @@ class ClientThread(Thread): self.bma_endpoints = ["BMAS "+host for host in conf["Client"]["BMA_Hosts"].split(",")] self.work = True + self.proxy = None + if conf["Client"]["Proxy"] != "": + _proxy = conf["Client"]["Proxy"].split(" ") + self.proxy = (_proxy[0], int(_proxy[1])) + self.proxy_onion_only = conf["Client"]["Proxy_OnionOnly"] == "1" def detectPeers(self):# Check known peers and ask them for their known peer list logPrint("Start peers detection", LOG_TRACE) @@ -483,16 +498,16 @@ class ClientThread(Thread): message = ubjson.dumpb({ "pubkey": self.keys.pubkey, - "host": self.conf["Server"]["Host"], - "port": self.conf["Server"]["Port"], + "host": self.conf["Server"]["Public_Host"], + "port": self.conf["Server"]["Public_Port"], "time": time.time() }) message = peer.keys.encrypt_seal(message) # Encrypt message = self.keys.sign(message) # Sign logPrint("Ask "+str(peer), LOG_TRACE) try: - header, content = sdata((peer.host, peer.port), "POST", "/list/new/"+self.keys.pubkey, message) # Send - except ConnectionRefusedError: + header, content = sdata((peer.host, peer.port), "POST", "/list/new/"+self.keys.pubkey, message, proxy=self.proxy, proxy_onion_only=self.proxy_onion_only) # Send + except (ConnectionRefusedError, socks.GeneralProxyError): peer.up = False logPrint("Down "+str(peer), LOG_TRACE) continue @@ -619,7 +634,7 @@ class ClientThread(Thread): message = self.keys.sign(tx.out_comment.encode() + tx.message) # Sign try: - header, content = sdata((peer.host, peer.port), "POST", "/mix/"+self.keys.pubkey+"/"+tx.out_amount+"/"+tx.out_base, message) + header, content = sdata((peer.host, peer.port), "POST", "/mix/"+self.keys.pubkey+"/"+tx.out_amount+"/"+tx.out_base, message, proxy=self.proxy, proxy_onion_only=self.proxy_onion_only) data = ubjson.loadb(content) assert data["mix_ok"] == tx.out_comment tx.need_send = False @@ -627,7 +642,7 @@ class ClientThread(Thread): peer.up = True logPrint("Sent "+tx.sender_pubkey[:8]+" -> "+tx.receiver_pubkey[:8]+" = "+str(tx.in_amount)+":"+str(tx.in_base)+" -> "+str(tx.out_amount)+":"+str(tx.out_base), LOG_TRACE) logPrint("Up "+str(peer), LOG_TRACE) - except ConnectionRefusedError: + except (ConnectionRefusedError, socks.GeneralProxyError): peer.up = False logPrint("Down "+str(peer), LOG_TRACE) except (ubjson.decoder.DecoderException, KeyError, AssertionError): @@ -643,7 +658,7 @@ class ClientThread(Thread): message = tx.genMixConfirm(self.keys) try: - header, content = sdata((peer.host, peer.port), "POST", "/confirm/"+self.keys.pubkey+"/"+tx.in_comment, message) + header, content = sdata((peer.host, peer.port), "POST", "/confirm/"+self.keys.pubkey+"/"+tx.in_comment, message, proxy=self.proxy, proxy_onion_only=self.proxy_onion_only) data = ubjson.loadb(content) assert data["confirm_ok"] == tx.in_comment tx.need_confirm = False @@ -651,7 +666,7 @@ class ClientThread(Thread): peer.up = True logPrint("Confirmed "+tx.sender_pubkey[:8]+" -> "+tx.receiver_pubkey[:8]+" = "+str(tx.in_amount)+":"+str(tx.in_base)+" -> "+str(tx.out_amount)+":"+str(tx.out_base), LOG_TRACE) logPrint("Up "+str(peer), LOG_TRACE) - except ConnectionRefusedError: + except (ConnectionRefusedError, socks.GeneralProxyError): peer.up = False logPrint("Down "+str(peer), LOG_TRACE) except (ubjson.decoder.DecoderException, KeyError, AssertionError): diff --git a/utils.py b/utils.py index f5dff52d2ffed7163990ca0a3c2244b09ec606cf..3b4c70c345515f477302979cd9c709d3a49d1579 100644 --- a/utils.py +++ b/utils.py @@ -17,8 +17,12 @@ """ import sys, os, re, socket, time +import socks from duniterpy.key import SigningKey, PublicKey +_argv = sys.argv # silkaj reads sys.argv! +sys.argv = [] import silkaj.money, silkaj.tx, silkaj.auth +sys.argv = _argv #-------- DATA @@ -47,11 +51,15 @@ RECBUF = 1024 p_clen = re.compile("\r?\ncontent-length: *(\d+)\r?\n?", re.IGNORECASE) -def sdata(host, mode, url="/", data=b"", uagent="GMixer-py"): +def sdata(host, mode, url="/", data=b"", uagent="GMixer-py", proxy=None, proxy_onion_only=False): + tsocket = socket.socket + if proxy and (not proxy_onion_only or re.match("^.+\.onion$", host[0])): + socks.set_default_proxy(socks.PROXY_TYPE_SOCKS5, proxy[0], proxy[1]) + tsocket = socks.socksocket if ":" in host[0]: # IPv6 - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock = tsocket(socket.AF_INET6, socket.SOCK_STREAM) else: # IPv4 - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock = tsocket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(host) raw = (mode+" "+url+" HTTP/1.1\r\nHost: "+host[0]+":"+str(host[1])+"\r\nUser-Agent: "+uagent+"\r\nAccept: */*\r\nContent-Length: "+str(len(data))+"\r\n\r\n").encode()+data