leylines server initial commit
This commit is contained in:
commit
d064dd93d4
|
@ -0,0 +1,10 @@
|
||||||
|
.venv
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
__pycache__
|
||||||
|
*.egg-info
|
||||||
|
.env
|
||||||
|
.vimrc
|
||||||
|
.ycm_extra_conf.py
|
||||||
|
*.db
|
|
@ -0,0 +1,9 @@
|
||||||
|
.PHONY: check install
|
||||||
|
|
||||||
|
check:
|
||||||
|
@mypy . || true
|
||||||
|
@flake8 . || true
|
||||||
|
@pyflakes . || true
|
||||||
|
|
||||||
|
install:
|
||||||
|
pip install --user -e .
|
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
||||||
|
from leylines import net_init, create_or_get_interface, sync_interface
|
||||||
|
|
||||||
|
net_init()
|
||||||
|
create_or_get_interface()
|
||||||
|
sync_interface()
|
|
@ -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()
|
|
@ -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)
|
Loading…
Reference in New Issue