feat: initialize project structure with core setup

Initial setup of the ColdBrew Django-based VPN manager project. Added basic project structure including core Django configurations and essential files:
- Added `.gitignore` to exclude unnecessary files from version control.
- Introduced `LICENSE` with MIT license.
- Created basic `README.md` with setup and contribution guidance.
- Configured Django project settings with auto-generated secrets.
- Implemented essential VPN management models, views, and admin settings.
- Integrated core dependencies like Django Rest Framework, Celery for task management, and Redis for caching.
- Added Poetry for dependency management along with necessary dependencies for development and database support.
This commit is contained in:
Kumi 2024-07-12 08:31:39 +02:00
commit e1bf6b9901
Signed by: kumi
GPG key ID: ECBCC9082395383F
22 changed files with 2151 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
settings.ini
*.pyc
__pycache__/
db.sqlite3
.venv/
venv/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 Private.coffee Team <support@private.coffee>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13
README.md Normal file
View file

@ -0,0 +1,13 @@
# ColdBrew - A Django-based VPN manager.
## Setup Notes
1. Ensure that you have a rule in iptables that denies routing by default. ColdBrew will manage the rules for you, but it needs a rule to start from. If you don't have one, you can add one with `iptables -A FORWARD -j DROP`.
## Contributing
We welcome contributions to this project. Please see the [CONTRIBUTING](CONTRIBUTING.md) file for more information.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

0
coldbrew/__init__.py Normal file
View file

16
coldbrew/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for coldbrew project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coldbrew.settings')
application = get_asgi_application()

130
coldbrew/settings.py Normal file
View file

@ -0,0 +1,130 @@
"""
Django settings for coldbrew project.
Generated by 'django-admin startproject' using Django 5.0.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
import os
import base64
from autosecretkey import AutoSecretKey
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
ASK = AutoSecretKey("settings.ini")
SECRET_KEY = ASK.secret_key
CONFIG = ASK.config
DEBUG = CONFIG.getboolean("ColdBrew", "Debug", fallback=False)
if not (FIELD_ENCRYPTION_KEY := CONFIG.get("ColdBrew", "EncryptionKey")):
FIELD_ENCRYPTION_KEY = base64.urlsafe_b64encode(os.urandom(32))
CONFIG["ColdBrew"]["EncryptionKey"] = FIELD_ENCRYPTION_KEY
ASK.write()
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"encrypted_model_fields",
"coldbrew.vpn",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "coldbrew.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "coldbrew.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

22
coldbrew/urls.py Normal file
View file

@ -0,0 +1,22 @@
"""
URL configuration for coldbrew project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]

0
coldbrew/vpn/__init__.py Normal file
View file

3
coldbrew/vpn/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
coldbrew/vpn/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class VpnConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "coldbrew.vpn"

26
coldbrew/vpn/fields.py Normal file
View file

@ -0,0 +1,26 @@
from django.db import models
import ipaddress
class SubnetField(models.CharField):
description = "A field to store IPv4 or IPv6 subnets"
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 43 # Maximum length for IPv6 with subnet
super().__init__(*args, **kwargs)
def from_db_value(self, value, expression, connection):
if value is None:
return value
return ipaddress.ip_network(value)
def to_python(self, value):
if value is None:
return value
if isinstance(value, ipaddress._BaseNetwork):
return value
return ipaddress.ip_network(value)
def get_prep_value(self, value):
if value is None:
return value
return str(value)

60
coldbrew/vpn/helpers.py Normal file
View file

@ -0,0 +1,60 @@
import subprocess
def generate_private_key() -> str:
"""Returns a new WireGuard private key
Returns:
str: The private key
"""
return subprocess.run(["wg", "genkey"], capture_output=True).stdout.decode().strip()
def get_public_key(private_key: str) -> str:
"""Returns the public key for a WireGuard private key
Args:
private_key (str): The private key
Returns:
str: The public key
"""
return subprocess.run(
["wg", "pubkey"], capture_output=True, input=private_key, text=True
).stdout.strip()
def generate_preshared_key() -> str:
"""Returns a new WireGuard preshared key
Returns:
str: The preshared key
"""
return subprocess.run(["wg", "genpsk"], capture_output=True).stdout.decode().strip()
def run_command(command):
"""Run a shell command and return the output
Args:
command (str): The command to run
Returns:
str: The output of the command
"""
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Command failed: {command}\n{result.stderr}")
return result.stdout.strip()
def get_interface_name(vpn):
"""Return the WireGuard interface name for a VPN
Args:
vpn (VPN): The VPN object
Returns:
str: The interface name
"""
return f"wg-{vpn.id}"

View file

43
coldbrew/vpn/models.py Normal file
View file

@ -0,0 +1,43 @@
from django.db import models
from django.contrib.auth import get_user_model
from uuid import uuid4
import random
from encrypted_model_fields.fields import EncryptedCharField
from .fields import SubnetField
from .helpers import generate_private_key, get_public_key
class VPN(models.Model):
id = models.UUIDField(default=uuid4, primary_key=True)
owner = models.ForeignKey(get_user_model(), models.CASCADE)
name = models.CharField(max_length=128)
port = models.IntegerField(
unique=True, min_value=10000, max_value=40000, null=True, blank=True
)
private_key = EncryptedCharField(max_length=128, null=True, blank=True)
public_key = models.CharField(max_length=128, null=True, blank=True)
ipv4_subnet = SubnetField(unique=True)
ipv6_subnet = SubnetField(unique=True)
def save(self, *args, **kwargs):
if not self.private_key:
self.private_key = generate_private_key()
self.public_key = get_public_key(self.private_key)
if not self.port:
self.port = random.randint(10000, 40000)
super().save(*args, **kwargs)
class Device(models.Model):
id = models.UUIDField(default=uuid4, primary_key=True)
vpn = models.ForeignKey(VPN, models.CASCADE)
name = models.CharField(max_length=128)
public_key = models.CharField(max_length=128)
preshared_key = EncryptedCharField(max_length=128)

3
coldbrew/vpn/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
coldbrew/vpn/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

@ -0,0 +1,43 @@
from .helpers import run_command, get_interface_name
def configure_interface(vpn):
interface_name = get_interface_name(vpn)
# Create and configure the interface
run_command(f"ip link add dev {interface_name} type wireguard")
run_command(f"ip address add {vpn.ipv4_subnet} dev {interface_name}")
run_command(f"ip address add {vpn.ipv6_subnet} dev {interface_name}")
run_command(
f"wg set {interface_name} listen-port {vpn.port} private-key <(echo {vpn.private_key})"
)
run_command(f"ip link set up dev {interface_name}")
def update_interface(vpn):
interface_name = get_interface_name(vpn)
# Update the interface configuration
run_command(f"ip address flush dev {interface_name}")
run_command(f"ip address add {vpn.ipv4_subnet} dev {interface_name}")
run_command(f"ip address add {vpn.ipv6_subnet} dev {interface_name}")
run_command(
f"wg set {interface_name} listen-port {vpn.port} private-key <(echo {vpn.private_key})"
)
def delete_interface(vpn):
interface_name = get_interface_name(vpn)
run_command(f"ip link delete dev {interface_name}")
def add_peer(vpn, device):
interface_name = get_interface_name(vpn)
run_command(
f"wg set {interface_name} peer {device.public_key} preshared-key <(echo {device.preshared_key}) allowed-ips {device.allowed_ips}"
)
def remove_peer(vpn, device):
interface_name = get_interface_name(vpn)
run_command(f"wg set {interface_name} peer {device.public_key} remove")

View file

@ -0,0 +1,54 @@
from .helpers import run_command, get_interface_name
def delete_interface_rules(interface_name):
# List existing IPv4 rules and delete those involving the interface
rules = run_command("iptables -S FORWARD").splitlines()
for rule in rules:
if interface_name in rule:
run_command(f"iptables {rule.replace('-A', '-D')}")
# List existing IPv6 rules and delete those involving the interface
rules = run_command("ip6tables -S FORWARD").splitlines()
for rule in rules:
if interface_name in rule:
run_command(f"ip6tables {rule.replace('-A', '-D')}")
def setup_firewall(vpn):
interface_name = get_interface_name(vpn)
# Remove any existing rules for this VPN interface
delete_interface_rules(interface_name)
# Allow forwarding only within the VPN's own subnets
run_command(
f"iptables -A FORWARD -i {interface_name} -s {vpn.ipv4_subnet} -d {vpn.ipv4_subnet} -j ACCEPT"
)
run_command(
f"ip6tables -A FORWARD -i {interface_name} -s {vpn.ipv6_subnet} -d {vpn.ipv6_subnet} -j ACCEPT"
)
def add_firewall_rule(vpn, target_vpn):
interface_name = get_interface_name(vpn)
# Allow forwarding from vpn to target_vpn
run_command(
f"iptables -A FORWARD -i {interface_name} -s {vpn.ipv4_subnet} -d {target_vpn.ipv4_subnet} -j ACCEPT"
)
run_command(
f"ip6tables -A FORWARD -i {interface_name} -s {vpn.ipv6_subnet} -d {target_vpn.ipv6_subnet} -j ACCEPT"
)
def remove_firewall_rule(vpn, target_vpn):
interface_name = get_interface_name(vpn)
# Remove forwarding rule from vpn to target_vpn
run_command(
f"iptables -D FORWARD -i {interface_name} -s {vpn.ipv4_subnet} -d {target_vpn.ipv4_subnet} -j ACCEPT || true"
)
run_command(
f"ip6tables -D FORWARD -i {interface_name} -s {vpn.ipv6_subnet} -d {target_vpn.ipv6_subnet} -j ACCEPT || true"
)

16
coldbrew/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for coldbrew project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coldbrew.settings')
application = get_wsgi_application()

22
manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coldbrew.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

1613
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

53
pyproject.toml Normal file
View file

@ -0,0 +1,53 @@
[tool.poetry]
name = "coldbrew"
version = "0.0.1"
description = ""
authors = ["Private.coffee Team <support@private.coffee>"]
license = "MIT"
readme = "README.md"
homepage = "https://private.coffee"
repository = "https://git.private.coffee/PrivateCoffee/coldbrew"
[tool.poetry.dependencies]
python = "^3.10"
django = "^5.0"
djangorestframework = "*"
django-storages = "*"
django-polymorphic = "*"
setuptools = "*"
pillow = "*"
pygments = "*"
markdown = "*"
coreapi = "*"
pyyaml = "*"
django-autosecretkey = "*"
celery = "*"
redis = "*"
django-celery-results = "*"
django-celery-beat = "*"
drf-spectacular = {extras = ["sidecar"], version = "*"}
boto3 = "*"
argon2-cffi = "*"
django-csp = "*"
django-rest-polymorphic = "*"
django-crispy-forms = "*"
crispy-bootstrap5 = "*"
django-encrypted-model-fields = "*"
[tool.poetry.group.mysql.dependencies]
mysqlclient = "*"
[tool.poetry.group.postgres.dependencies]
psycopg2 = "*"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
ruff = "*"
black = "*"
[tool.poetry.scripts]
quackscape-manage = "quackscape.manage:main"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"