leylines server initial commit

This commit is contained in:
xenia 2021-06-14 04:45:02 -04:00
commit d064dd93d4
6 changed files with 294 additions and 0 deletions

10
leylines/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.venv
*.pyc
*.pyo
*.pyd
__pycache__
*.egg-info
.env
.vimrc
.ycm_extra_conf.py
*.db

9
leylines/Makefile Normal file
View File

@ -0,0 +1,9 @@
.PHONY: check install
check:
@mypy . || true
@flake8 . || true
@pyflakes . || true
install:
pip install --user -e .

View File

@ -0,0 +1,108 @@
import binascii
from typing import TypeVar, List, Tuple, Optional
from pyroute2 import IPRoute, WireGuard
import monocypher
from .database import Database, Node, SERVER_NODE_ID
IFNAME = 'leyline-wg'
DEFAULT_PORT = 31337
db = Database("leylines.db")
def net_init() -> None:
with open("/proc/sys/net/ipv4/ip_forward", "w") as f:
f.write("1")
def seckey_to_pubkey(seckey: str) -> str:
seckey_bytes = binascii.a2b_base64(seckey)
pubkey_bytes = monocypher.crypto_key_exchange_public_key(seckey_bytes)
return binascii.b2a_base64(pubkey_bytes).decode().strip()
def get_server_node() -> Node:
server_node = db.get_server_node()
if server_node is None:
raise Exception("no server node defined")
return server_node
def generate_node_config(id: int) -> Optional[str]:
node = db.get_node(id)
if node is None:
return None
server_node = get_server_node()
interface = db.get_subnet()
return (
"[Interface]\n" +
f"PrivateKey={node.seckey}\n"
f"Address={node.ip}/{interface.network.prefixlen}\n"
"[Peer]\n"
f"PublicKey={seckey_to_pubkey(server_node.seckey)}\n"
f"AllowedIPs={interface.network}\n"
f"Endpoint={server_node.public_ip}:{DEFAULT_PORT}\n"
)
def add_node_to_wg(wg: WireGuard, node: Node) -> None:
if node.id == SERVER_NODE_ID:
return
peer = {
"public_key": seckey_to_pubkey(node.seckey),
"persistent_keepalive": 60,
"allowed_ips": [f"{node.ip}/32"]
}
wg.set(IFNAME, peer=peer)
def create_or_get_interface() -> int:
with IPRoute() as ipr:
lookup = ipr.link_lookup(ifname=IFNAME)
if len(lookup) > 0:
return lookup[0]
server_node = get_server_node()
interface = db.get_subnet()
ipr.link("add", ifname=IFNAME, kind="wireguard")
dev = ipr.link_lookup(ifname=IFNAME)[0]
ipr.link("set", index=dev, state="down")
ipr.addr("add", index=dev, address=str(server_node.ip), mask=interface.network.prefixlen,
broadcast=str(interface.network.broadcast_address))
ipr.link("set", index=dev, state="up")
with WireGuard() as wg:
wg.set(IFNAME, private_key=server_node.seckey, listen_port=DEFAULT_PORT)
return dev
def delete_interface() -> None:
with IPRoute() as ipr:
ipr.link("del", ifname=IFNAME)
def sync_interface() -> None:
create_or_get_interface()
with WireGuard() as wg:
info = wg.info(IFNAME)[0].get_attr("WGDEVICE_A_PEERS")
if info is None:
info = []
node_keys = {
seckey_to_pubkey(node.seckey): node
for node in db.get_nodes() if node.id != SERVER_NODE_ID
}
for peer in info:
pubkey = peer.get_attr("WGPEER_A_PUBLIC_KEY")
if pubkey in node_keys:
del node_keys[pubkey]
else:
wg.set(IFNAME, peer={"public_key": pubkey, "remove": True})
for node in node_keys.values():
add_node_to_wg(wg, node)

View File

@ -0,0 +1,5 @@
from leylines import net_init, create_or_get_interface, sync_interface
net_init()
create_or_get_interface()
sync_interface()

View File

@ -0,0 +1,142 @@
import binascii
import ipaddress
import secrets
import sqlite3
from typing import Iterable, List, NamedTuple, Optional, Set
import monocypher
DEFAULT_DB_PATH = "/var/lib/leylines/leylines.db"
DEFAULT_SUBNET = "172.16.10.1/24"
SERVER_NODE_ID = 1
class Node(NamedTuple):
id: int
name: str
public_ip: Optional[ipaddress.IPv4Address]
ip: ipaddress.IPv4Address
seckey: str
ssh_key: str
class Database:
__slots__ = ["conn"]
conn: sqlite3.Connection
def __init__(self, path=DEFAULT_DB_PATH) -> None:
self.conn = sqlite3.connect(path)
self.conn.execute("PRAGMA foreign_keys = 1")
self.conn.execute(
"""CREATE TABLE IF NOT EXISTS nodes (id INTEGER PRIMARY KEY, name TEXT NOT NULL,
public_ip TEXT, ip TEXT NOT NULL, seckey TEXT NOT NULL,
ssh_key TEXT NOT NULL)""")
self.conn.execute(
"""CREATE TABLE IF NOT EXISTS node_resources
(node INTEGER NOT NULL, resource TEXT NOT NULL, PRIMARY KEY (node, resource),
FOREIGN KEY(node) REFERENCES nodes(id) ON DELETE CASCADE)""")
self.conn.execute(
"CREATE TABLE IF NOT EXISTS settings (name TEXT PRIMARY KEY, value TEXT NOT NULL)")
self.conn.execute(
"INSERT OR IGNORE INTO settings (name, value) VALUES ('subnet', ?)", (DEFAULT_SUBNET,))
self.conn.commit()
def init_server(self, name: str, public_ip: ipaddress.IPv4Address) -> None:
if self.get_node(SERVER_NODE_ID) is not None:
raise Exception("there is already a server node defined")
self.add_node(name, public_ip, "lol no")
def get_subnet(self) -> ipaddress.IPv4Interface:
cur = self.conn.cursor()
cur.execute("SELECT value FROM settings WHERE name='subnet'")
return ipaddress.IPv4Interface(cur.fetchone()[0])
def _get_free_ip(self) -> ipaddress.IPv4Address:
subnet = self.get_subnet().network
subnets = [self.get_subnet().network]
ips = [n.ip for n in self.get_nodes()]
ips.extend([subnet.network_address, subnet.broadcast_address])
for ip in ips:
ip_net = ipaddress.IPv4Network(ip)
new_subnets: List[ipaddress.IPv4Network] = []
for subnet in subnets:
if subnet.overlaps(ip_net):
new_subnets.extend(subnet.address_exclude(ip_net))
else:
new_subnets.append(subnet)
subnets = new_subnets
if len(subnets) == 0:
raise Exception("no more ips left to allocate!")
return next(subnets[0].hosts())
def add_node(self, name: str, public_ip: Optional[ipaddress.IPv4Address], ssh_key: str) -> Node:
cur = self.conn.cursor()
ip = self._get_free_ip()
insert_public_ip: Optional[str] = None
if public_ip is not None:
insert_public_ip = str(public_ip)
seckey_bytes = monocypher.crypto_key_exchange_make_key()
seckey = binascii.b2a_base64(seckey_bytes).decode().strip()
cur.execute(
"INSERT INTO nodes (name, public_ip, ip, seckey, ssh_key) VALUES(?, ?, ?, ?, ?) RETURNING id",
(name, insert_public_ip, str(ip), seckey, ssh_key))
id: int = cur.fetchone()[0]
cur.close()
self.conn.commit()
return Node(id, name, public_ip, ip, seckey, ssh_key)
def get_node(self, id: int) -> Optional[Node]:
cur = self.conn.cursor()
cur.execute("SELECT id, name, public_ip, ip, seckey, ssh_key FROM nodes WHERE id=?", (id,))
row = cur.fetchone()
if row is None:
return None
cur.close()
public_ip: Optional[ipaddress.IPv4Address] = None
if row[2] is not None:
public_ip = ipaddress.IPv4Address(row[2])
return Node(row[0], row[1], public_ip, ipaddress.IPv4Address(row[3]), row[4], row[5])
def get_server_node(self) -> Optional[Node]:
return self.get_node(SERVER_NODE_ID)
def get_nodes(self) -> Iterable[Node]:
cur = self.conn.cursor()
cur.execute("SELECT id, name, public_ip, ip, seckey, ssh_key FROM nodes")
nodes = []
while True:
row = cur.fetchone()
if row is None:
break
public_ip: Optional[ipaddress.IPv4Address] = None
if row[2] is not None:
public_ip = ipaddress.IPv4Address(row[2])
nodes.append(Node(row[0], row[1], public_ip,
ipaddress.IPv4Address(row[3]), row[4], row[5]))
cur.close()
return nodes
def get_node_resources(self, id: int) -> Set[str]:
cur = self.conn.cursor()
cur.execute("SELECT resource FROM node_resources WHERE node=?", (id,))
resources = set()
while True:
row = cur.fetchone()
if row is None:
break
resources.add(row[0])
cur.close()
return resources
def add_node_resource(self, id: int, resource: str) -> None:
self.conn.execute("INSERT OR IGNORE INTO node_resources (node, resource) VALUES (?, ?)",
(id, resource))
self.conn.commit()
def remove_node_resource(self, id: int, resource: str) -> None:
self.conn.execute("DELETE FROM node_resources WHERE node=? and resource=?",
(id, resource))
self.conn.commit()

20
leylines/setup.py Normal file
View File

@ -0,0 +1,20 @@
from setuptools import setup
setup(name='leylines',
version='0.1',
description='Sample text',
url='https://awoo.systems',
author='haskal',
author_email='haskal@awoo.systems',
license='AGPLv3',
packages=['leylines'],
install_requires=[
# requirements
],
include_package_data=True,
entry_points={
'console_scripts': [
# bins
]
},
zip_safe=True)