Created
April 22, 2025 01:19
-
-
Save laanwj/13f88b993cc5c33826aef28dc629e37b to your computer and use it in GitHub Desktop.
Check and report status of joinmarket directory nodes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# Check and report status of joinmarket directory nodes. | |
# SPDX-License-Identifier: MIT | |
import socket | |
import subprocess | |
PROXY = ('127.0.0.1', 9050) | |
GLYPH = ['❌', '🆗'] | |
# Nodes to check | |
NODES = [ | |
'g3hv4uynnmynqqq2mchf3fcm3yd46kfzmcdogejuckgwknwyq5ya6iad.onion:5222', | |
'3kxw6lf5vf6y26emzwgibzhrzhmhqiw6ekrek3nqfjjmhwznb2moonad.onion:5222', | |
'bqlpq6ak24mwvuixixitift4yu42nxchlilrcqwk2ugn45tdclg42qid.onion:5222', | |
'wkd3kd73ze62sqhlj2ebwe6fxqvshw5sya6nkvrgcweegt7ljhuspaid.onion:5222', | |
'ylegp63psfqh3zk2huckf2xth6dxvh2z364ykjfmvsoze6tkfjceq7qd.onion:5222', | |
'plq5jw5hqke6werrc5duvgwbuczrg4mphlqsqbzmdfwxwkm2ncdzxeqd.onion:5222', | |
'odpwaf67rs5226uabcamvypg3y4bngzmfk7255flcdodesqhsvkptaid.onion:5222', | |
] | |
# Proxy handling # | |
### Utility functions | |
def recvall(s, n): | |
'''Receive n bytes from a socket, or fail''' | |
rv = bytearray() | |
while n > 0: | |
d = s.recv(n) | |
if not d: | |
raise IOError('Unexpected end of stream') | |
rv.extend(d) | |
n -= len(d) | |
return rv | |
### Protocol constants | |
class Command: | |
CONNECT = 0x01 | |
class AddressType: | |
IPV4 = 0x01 | |
DOMAINNAME = 0x03 | |
IPV6 = 0x04 | |
class Reply: | |
SUCCEEDED = 0x00 # RFC1928: Succeeded | |
GENFAILURE = 0x01 # RFC1928: General failure | |
NOTALLOWED = 0x02 # RFC1928: Connection not allowed by ruleset | |
NETUNREACHABLE = 0x03 # RFC1928: Network unreachable | |
HOSTUNREACHABLE = 0x04 # RFC1928: Network unreachable | |
CONNREFUSED = 0x05 # RFC1928: Connection refused | |
TTLEXPIRED = 0x06 # RFC1928: TTL expired | |
CMDUNSUPPORTED = 0x07 # RFC1928: Command not supported | |
ATYPEUNSUPPORTED = 0x08 # RFC1928: Address type not supported | |
TOR_HS_DESC_NOT_FOUND = 0xf0 # Tor: Onion service descriptor can not be found | |
TOR_HS_DESC_INVALID = 0xf1 # Tor: Onion service descriptor is invalid | |
TOR_HS_INTRO_FAILED = 0xf2 # Tor: Onion service introduction failed | |
TOR_HS_REND_FAILED = 0xf3 # Tor: Onion service rendezvous failed | |
TOR_HS_MISSING_CLIENT_AUTH = 0xf4 # Tor: Onion service missing client authorization | |
TOR_HS_WRONG_CLIENT_AUTH = 0xf5 # Tor: Onion service wrong client authorization | |
TOR_HS_BAD_ADDRESS = 0xf6 # Tor: Onion service invalid address | |
TOR_HS_INTRO_TIMEOUT = 0xf7 # Tor: Onion service introduction timed out | |
mapping = { | |
SUCCEEDED: 'Succeeded', | |
GENFAILURE: 'General failure', | |
NOTALLOWED: 'Connection not allowed by ruleset', | |
NETUNREACHABLE: 'Network unreachable', | |
HOSTUNREACHABLE: 'Host unreachable', | |
CONNREFUSED: 'Connection refused', | |
TTLEXPIRED: 'TTL expired', | |
CMDUNSUPPORTED: 'Command not supported', | |
ATYPEUNSUPPORTED: 'Address type not supported', | |
TOR_HS_DESC_NOT_FOUND: 'Onion service descriptor can not be found', | |
TOR_HS_DESC_INVALID: 'Onion service descriptor is invalid', | |
TOR_HS_INTRO_FAILED: 'Onion service introduction failed', | |
TOR_HS_REND_FAILED: 'Onion service rendezvous failed', | |
TOR_HS_MISSING_CLIENT_AUTH: 'Onion service missing client authorization', | |
TOR_HS_WRONG_CLIENT_AUTH: 'Onion service wrong client authorization', | |
TOR_HS_BAD_ADDRESS: 'Onion service invalid address', | |
TOR_HS_INTRO_TIMEOUT: 'Onion service introduction timed out', | |
} | |
def socks5_connect(sock, dst): | |
'''SOCKS5 negotiation. | |
sock must already be connected to proxy. | |
''' | |
(host, port) = dst | |
# version, nmethods, support unauthenticated | |
sock.sendall(bytes([0x05, 0x01, 0x00])) | |
# receive version, chosen method | |
rv = recvall(sock, 2) | |
if rv[0] != 0x05: | |
raise IOError(f"Invalid socks version {rv[0]} in auth response") | |
if rv[1] != 0x00: | |
raise IOError(f"Unsupported authentication method {rv[1]}") | |
# send CONNECT request | |
# ver,cmd,rsv,atyp,asize,addr[asize],port_hi,port_lo | |
assert(len(host) <= 255) | |
assert(port >= 0 and port < 0xffff) | |
sock.sendall(bytes([0x05, Command.CONNECT, 0x00, AddressType.DOMAINNAME, len(host)]) + | |
bytes(host.encode()) + | |
bytes([port >> 8, port & 0xFF])) | |
# receive reponse, including bind address and port (which we ignore) | |
# ver,status,rsv,atyp,[addr],port_hi,port_lo | |
rv = recvall(sock, 4) | |
if rv[0] != 0x05: | |
raise IOError(f"Invalid socks version {rv[0]} in response") | |
if rv[1] != 0x00: | |
raise IOError(f"{Reply.mapping.get(rv[1], 'Unknown')} (0x{rv[1]:02x})") | |
if rv[2] != 0x00: | |
raise IOError("SOCKS5 malformed response") | |
if rv[3] == AddressType.IPV4: | |
bindaddr = recvall(sock, 4) | |
elif rv[3] == AddressType.IPV6: | |
bindaddr = recvall(sock, 16) | |
elif rv[3] == AddressType.DOMAINNAME: | |
asize = recvall(sock, 1)[0] | |
bindaddr = recvall(sock, asize) | |
else: | |
raise IOError("SOCKS5 malformed response") | |
bindport = recvall(sock, 2) | |
def main(): | |
active = [] | |
for node in NODES: | |
(host, port) = node.split(':') | |
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
sock.connect(PROXY) | |
try: | |
socks5_connect(sock, (host, int(port))) | |
except IOError as e: | |
ok = False | |
status = str(e) | |
else: | |
ok = True | |
status = '' | |
active.append(node) | |
print(f'- {GLYPH[ok]} `{node}` {status}') | |
print('```') | |
print(f'directory_nodes = {",".join(active)}') | |
print('```') | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment