123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- #!/usr/bin/env python2
- """
- dynamicEndpoints.py: make cjdns reliably connect to remote nodes with dynamic IP
- addresses, identified by a DNS name.
- Requires a config file with a section for each dynamic-IP node, like this:
- [lhjs0njqtvh1z4p2922bbyp2mksmyzf5lb63kvs3ppy78y1dj130.k]
- hostname: verge.info.tm
- port: 6324
- password: ns6vn00hw0buhrtc4wbk8sv230
- The section name (in square brackets) is the public key of the node. Then the
- hostname, port, and peering password for the node are given.
- By default, this program looks up the current Internet IP of each node defined
- in the config file, and add that node at that IP to the local cjdns instance.
- Unless the --noWait option is given, or the $nowait environment variable is
- true, the program then continues running, waiting for cjdns to log messages
- about those peers being unresponsive and updating the peers' Internet IP
- addresses as needed.
- If cjdns dies while the program is monitoring for messages, the program will
- hang indefinitely.
- Requires that the $HOME/.cjdnsadmin file be correctly set up. See
- cjdnsadminmaker.py if that is not the case.
- """
- # You may redistribute this program 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 <https://www.gnu.org/licenses/>.
- from cjdnsadmin.cjdnsadmin import connectWithAdminInfo;
- from cjdnsadmin.publicToIp6 import PublicToIp6_convert;
- from cjdnsadmin.bencode import *
- import sys
- import socket, re
- import select
- import time
- import os
- import pwd
- import grp
- import logging
- import argparse
- import atexit
- import ConfigParser
- # This holds a regex that matches the message we get from the roiuter when it
- # sees an unresponsive peer.
- IS_UNRESPONSIVE = re.compile(
- "Pinging unresponsive peer \\[(.*\\.k)\\] lag \\[.*\\]")
- # Make sure that it works
- assert(IS_UNRESPONSIVE.match("Pinging unresponsive peer " +
- "[6fmmn3qurcjg6d8hplq1rrcsspfhvm1900s13f3p5bv2bb4f4mm0.k] lag [207147]"))
- # What file do these messages come from? TODO: don't depend so tightly on the
- # other end of the codebase. Use the API to watch peers.
- MESSAGE_FILE = "InterfaceController.c"
- MESSAGE_LINE = 0 # All lines
- class Node(object):
- """
- Represents a remote peer. A remoter peer has:
- - A hostname to repeatedly look up
- - A port to connect to
- - A password to connect with
- - A public key to authenticate the remote peer with
- - A last known Internet IP address.
- """
- __slots__ = ("host","port","password","key","lastAddr")
- def __init__(self,host,port,password,key):
- self.host = host
- self.port = port
- self.password = password
- self.key = key
- self.lastAddr = None
- class DynamicEndpointWatcher(object):
- """
- Encapsulates all the stuff we need to actually keep an eye on our remote
- nodes and see if they change IPs. When a node with a dynamic IP is
- unresponsive, we look up its IP address and tell cjdns to go connect to it.
- """
- def __init__(self, cjdns, configuration):
- """
- Set up a new DynamicEndpointWatcher operating on the given CJDNS admin
- connection, using the specified ConfigParser parsed configuration.
- """
- # Keep the cjdns admin connection
- self.cjdns = cjdns
- # Holds a dict from public key string to Node object for the remote
- # peer, for all known nodes.
- self.nodes = dict()
- # Holds a dict from public key to Node object for those nodes which are
- # unresponsive.
- self.unresponsive = dict()
- # Add nodes from the given ConfigParser parser.
- for section in configuration.sections():
- # Each section is named with a node key, and contains a
- # hostname, port, and password.
- peerHostname = configuration.get(section, "hostname")
- peerPort = configuration.get(section, "port")
- peerPassword = configuration.get(section, "password")
- # Add the node
- self.addNode(peerHostname, peerPort, peerPassword, section)
- # Add all the nodes we're supposed to watch.
- for node in self.nodes.values():
- self.lookup(node)
- logging.info("{} peers added!".format(len(self.nodes)))
- # Holds a cjdns log message subscription to messages about unresponsive
- # nodes.
- self.sub = self.cjdns.AdminLog_subscribe(MESSAGE_LINE, MESSAGE_FILE,
- 'DEBUG')
- if self.sub['error'] == 'none':
- # We successfully subscribed to messages.
-
- # When we die, try to unsubscribe
- atexit.register(self.stop)
-
- else:
- logging.error(self.sub)
- def run(self):
- """
- Run forever, monitoring the peers we are responsible for.
- """
- logging.info("Watching for unresponsive peers")
- # Watch for any messages from our log message subscription.
- self.recieve(self.sub['txid'])
-
- def stop(self):
- """
- Unsubscribe from the admin log and close the connection to cjdns because
- we are shutting down the program. If we don't do this, cjdns might
- crash. If we do do it, cjdns might still crash.
- """
- # Unsubscribe cleanly
- logging.info("Unsubscribing from stream {}".format(
- self.sub['streamId']))
- unsub = self.cjdns.AdminLog_unsubscribe(self.sub['streamId'])
- if unsub['error'] != 'none':
- logging.error(unsub['error'])
-
- # Close the connection
- logging.info("Closing admin connection")
- self.cjdns.disconnect()
- def addNode(self, host, port, password, key):
- """
- Add a new remote peer with the given hostname, port, password, and
- public key. Does not automatically try to connect to the remote node.
- """
- self.nodes[key] = Node(host, port, password, key)
- def lookup(self, node):
- """
- Look up the current IP address for the given Node object, and tell the
- cjdns router to try to connect to it.
- """
- try:
- # Use AF_INET here to make sure we don't get an IPv6 address and try
- # to connect to it when the cjdns UDPInterface is using only IPv4.
- # TODO: Make cjdns bind its UDPInterface to IPv6 as well as IPv4.
- for info in socket.getaddrinfo(node.host,node.port,
- socket.AF_INET,socket.SOCK_DGRAM):
- # For every IP address the node has in DNS, with the port we
- # wanted attached...
- # Save the address we get in a field in the node.
- sockaddr = info[-1]
- node.lastAddr = sockaddr
- # Grab the IP:port string
- sockaddr = sockaddr[0] + ":" + str(sockaddr[1])
- # Announce we are going to connect
- logging.info("Connecting to {} at {}".format(
- PublicToIp6_convert(node.key), sockaddr))
- # Tell CJDNS to begin a UDPInterface connection to the given
- # IP:port, with the given public key and password. Always use
- # the 0th UDPInterface, which is the default.
- reply = self.cjdns.UDPInterface_beginConnection(
- password=node.password, publicKey=node.key,
- address=sockaddr)
- if reply["error"] != "none":
- # The router didn't like our request. Complain.
- logging.error(
- "Router refuses to connect to remote peer. {}".format(
- reply["error"]))
- # Maybe try the next address?
- break
- # Mark this node as no longer unresponsive
- try: del self.unresponsive[node.key]
- except KeyError: pass
- # Don't try any more addresses. Stop after the first.
- return
- except socket.gaierror as e:
- # The lookup failed at the OS level. Did we put in a bad hostname?
- logging.error("Could not resolve DNS name {}: {}".format(
- node.host, e))
- # If we get here, we found no addresses that worked.
- logging.error("No working addresses found for node {}".format(
- PublicToIp6_convert(node.key)))
- def doLog(self, message):
- """
- Process a log line from cjdns to see if it indicates that a peer we are
- responsible for is unresponsive.
- """
- logging.debug(message)
- # Short-circuit messages that start with the wrong l;etter and can't
- # possibly match.
- if message[0]!='P': return;
- # Test plausible messages against the regex
- p = IS_UNRESPONSIVE.match(message)
- # If they don't match, ignore them.
- if not p: return
- # Otherwise, get the key of the unresponsive node from the regex match
- # group.
- downKey = p.group(1)
- # And get the nodfe for that key
- node = self.nodes.get(downKey,None)
- if not node:
- # Complain we aren't responsible for that node.
- logging.debug("Unmonitored neighbor {} is down".format(
- PublicToIp6_convert(downKey)))
- return
- # Otherwise, announce we are doing our job.
- logging.warning("Monitored neighbor {} is down.".format(
- PublicToIp6_convert(downKey)))
- # If we are responsible for it, register it as unresponsive.
- self.unresponsive[downKey] = node
- # Go get its address and try reconnecting.
- self.lookup(node)
- def recieve(self, txid):
- """
- Loop forever porcessing messages from the cjdns router. Takes a txid
- pointing to the subscription to such messages.
- """
- while True:
- # Repeatedly get and process log messages.
- self.doLog(self.cjdns.getMessage(txid)["message"])
- def main(argv):
- """
- Run the program.
- """
- # Set up logging. See the logging module docs.
- logging.basicConfig(format="[%(asctime)s] %(levelname)s: %(message)s",
- level=logging.INFO)
- # Parse command-line arguments. Make sure to give our docstring as program
- # help.
- parser = argparse.ArgumentParser(description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
- parser.add_argument("configFile", type=argparse.FileType("r"),
- help="configuration file of hosts to read")
- parser.add_argument("--noWait", action="store_true",
- help="look up dynamic peers once and exit")
- parser.add_argument("--adminInfo",
- help="use this file to load the cjdns admin password")
- # Parse all the command-line arguments
- options = parser.parse_args(argv[1:])
- while True:
- try:
- # Connect to the router, using the specified admin info file, if
- # given.
- cjdns = connectWithAdminInfo(path=options.adminInfo)
- break
- except socket.error:
- # Connection probably refused. Retry in a bit
- logging.error("Error connecting to cjdns. Retrying in 1 minute...")
- time.sleep(60)
-
- # Drop root if we have it. We can't do it before we load the admin info
- # file, for the use case where that file is root-only.
- try:
- # Switch group to nogroup
- os.setgid(grp.getgrnam("nogroup").gr_gid)
- # Switch user to nobody
- os.setuid(pwd.getpwnam("nobody").pw_uid)
- # Announce we dropped privs
- logging.info("Dropped privileges: running as {}:{}".format(
- pwd.getpwuid(os.getuid())[0], grp.getgrgid(os.getgid())[0]))
- except (OSError,KeyError):
- # Complain we couldn't drop privs right
- logging.warning("Could not drop privileges: running as {}:{}".format(
- pwd.getpwuid(os.getuid())[0], grp.getgrgid(os.getgid())[0]))
- # Now we can load the config file. It is now required.
- # Maker a new parser to parse the config file
- parsedConfig = ConfigParser.SafeConfigParser()
- # Be case sensitive
- parsedConfig.optionxform = str
- # Read the config from the file
- parsedConfig.readfp(options.configFile)
- # Make a new watcher on the cjdroute connection, with the config from the
- # config file. This automatically looks up all the peers and tries to
- # connect to them once.
- watcher = DynamicEndpointWatcher(cjdns, parsedConfig)
- if options.noWait or os.environ.get('nowait', False):
- # We're not supposed to wait. Quit while we're ahead
- sys.exit(0)
- else:
- # Monitor for unresponsive nodes. This will loop until cjdns restarts,
- # at which point it will throw an exception.
- watcher.run()
- if __name__ == "__main__":
- # Run our main method
- sys.exit(main(sys.argv))
|