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