Fork project
This commit is contained in:
		
							
								
								
									
										0
									
								
								utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										150
									
								
								utils/cache.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								utils/cache.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,150 @@
 | 
			
		||||
import ipaddress
 | 
			
		||||
import warnings
 | 
			
		||||
from CTFd.cache import cache
 | 
			
		||||
from CTFd.utils import get_config
 | 
			
		||||
from flask_redis import FlaskRedis
 | 
			
		||||
from redis.exceptions import LockError
 | 
			
		||||
 | 
			
		||||
from .db import DBContainer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CacheProvider:
 | 
			
		||||
    def __init__(self, app, *args, **kwargs):
 | 
			
		||||
        if app.config['CACHE_TYPE'] == 'redis':
 | 
			
		||||
            self.provider = RedisCacheProvider(app, *args, **kwargs)
 | 
			
		||||
        elif app.config['CACHE_TYPE'] in ['filesystem', 'simple']:
 | 
			
		||||
            if not hasattr(CacheProvider, 'cache'):
 | 
			
		||||
                CacheProvider.cache = {}
 | 
			
		||||
            self.provider = FilesystemCacheProvider(app, *args, **kwargs)
 | 
			
		||||
            self.init_port_sets()
 | 
			
		||||
 | 
			
		||||
    def init_port_sets(self):
 | 
			
		||||
        self.clear()
 | 
			
		||||
 | 
			
		||||
        containers = DBContainer.get_all_container()
 | 
			
		||||
        used_port_list = []
 | 
			
		||||
        for container in containers:
 | 
			
		||||
            if container.port != 0:
 | 
			
		||||
                used_port_list.append(container.port)
 | 
			
		||||
        for port in range(int(get_config("whale:frp_direct_port_minimum", 29000)),
 | 
			
		||||
                          int(get_config("whale:frp_direct_port_maximum", 28000)) + 1):
 | 
			
		||||
            if port not in used_port_list:
 | 
			
		||||
                self.add_available_port(port)
 | 
			
		||||
 | 
			
		||||
        from .docker import get_docker_client
 | 
			
		||||
        client = get_docker_client()
 | 
			
		||||
 | 
			
		||||
        docker_subnet = get_config("whale:docker_subnet", "174.1.0.0/16")
 | 
			
		||||
        docker_subnet_new_prefix = int(
 | 
			
		||||
            get_config("whale:docker_subnet_new_prefix", "24"))
 | 
			
		||||
 | 
			
		||||
        exist_networks = []
 | 
			
		||||
        available_networks = []
 | 
			
		||||
 | 
			
		||||
        for network in client.networks.list(filters={'label': 'prefix'}):
 | 
			
		||||
            exist_networks.append(str(network.attrs['Labels']['prefix']))
 | 
			
		||||
 | 
			
		||||
        for network in list(ipaddress.ip_network(docker_subnet).subnets(new_prefix=docker_subnet_new_prefix)):
 | 
			
		||||
            if str(network) not in exist_networks:
 | 
			
		||||
                available_networks.append(str(network))
 | 
			
		||||
 | 
			
		||||
        self.add_available_network_range(*set(available_networks))
 | 
			
		||||
 | 
			
		||||
    def __getattr__(self, name):
 | 
			
		||||
        return self.provider.__getattribute__(name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FilesystemCacheProvider:
 | 
			
		||||
    def __init__(self, app, *args, **kwargs):
 | 
			
		||||
        warnings.warn(
 | 
			
		||||
            '\n[CTFd Whale] Warning: looks like you are using filesystem cache. '
 | 
			
		||||
            '\nThis is for TESTING purposes only, DO NOT USE on production sites.',
 | 
			
		||||
            RuntimeWarning
 | 
			
		||||
        )
 | 
			
		||||
        self.key = 'ctfd_whale_lock-' + str(kwargs.get('user_id', 0))
 | 
			
		||||
        self.global_port_key = "ctfd_whale-port-set"
 | 
			
		||||
        self.global_network_key = "ctfd_whale-network-set"
 | 
			
		||||
 | 
			
		||||
    def clear(self):
 | 
			
		||||
        cache.set(self.global_port_key, set())
 | 
			
		||||
        cache.set(self.global_network_key, set())
 | 
			
		||||
 | 
			
		||||
    def add_available_network_range(self, *ranges):
 | 
			
		||||
        s = cache.get(self.global_network_key)
 | 
			
		||||
        s.update(ranges)
 | 
			
		||||
        cache.set(self.global_network_key, s)
 | 
			
		||||
 | 
			
		||||
    def get_available_network_range(self):
 | 
			
		||||
        try:
 | 
			
		||||
            s = cache.get(self.global_network_key)
 | 
			
		||||
            r = s.pop()
 | 
			
		||||
            cache.set(self.global_network_key, s)
 | 
			
		||||
            return r
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def add_available_port(self, port):
 | 
			
		||||
        s = cache.get(self.global_port_key)
 | 
			
		||||
        s.add(port)
 | 
			
		||||
        cache.set(self.global_port_key, s)
 | 
			
		||||
 | 
			
		||||
    def get_available_port(self):
 | 
			
		||||
        try:
 | 
			
		||||
            s = cache.get(self.global_port_key)
 | 
			
		||||
            r = s.pop()
 | 
			
		||||
            cache.set(self.global_port_key, s)
 | 
			
		||||
            return r
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def acquire_lock(self):
 | 
			
		||||
        # for testing purposes only, so no need to set this limit
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def release_lock(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RedisCacheProvider(FlaskRedis):
 | 
			
		||||
    def __init__(self, app, *args, **kwargs):
 | 
			
		||||
        super().__init__(app)
 | 
			
		||||
        self.key = 'ctfd_whale_lock-' + str(kwargs.get('user_id', 0))
 | 
			
		||||
        self.current_lock = None
 | 
			
		||||
        self.global_port_key = "ctfd_whale-port-set"
 | 
			
		||||
        self.global_network_key = "ctfd_whale-network-set"
 | 
			
		||||
 | 
			
		||||
    def clear(self):
 | 
			
		||||
        self.delete(self.global_port_key)
 | 
			
		||||
        self.delete(self.global_network_key)
 | 
			
		||||
 | 
			
		||||
    def add_available_network_range(self, *ranges):
 | 
			
		||||
        self.sadd(self.global_network_key, *ranges)
 | 
			
		||||
 | 
			
		||||
    def get_available_network_range(self):
 | 
			
		||||
        return self.spop(self.global_network_key).decode()
 | 
			
		||||
 | 
			
		||||
    def add_available_port(self, port):
 | 
			
		||||
        self.sadd(self.global_port_key, str(port))
 | 
			
		||||
 | 
			
		||||
    def get_available_port(self):
 | 
			
		||||
        return int(self.spop(self.global_port_key))
 | 
			
		||||
 | 
			
		||||
    def acquire_lock(self):
 | 
			
		||||
        lock = self.lock(name=self.key, timeout=10)
 | 
			
		||||
 | 
			
		||||
        if not lock.acquire(blocking=True, blocking_timeout=2.0):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        self.current_lock = lock
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def release_lock(self):
 | 
			
		||||
        if self.current_lock is None:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.current_lock.release()
 | 
			
		||||
 | 
			
		||||
            return True
 | 
			
		||||
        except LockError:
 | 
			
		||||
            return False
 | 
			
		||||
							
								
								
									
										50
									
								
								utils/checks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								utils/checks.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
from docker.errors import DockerException, TLSParameterError, APIError, requests
 | 
			
		||||
 | 
			
		||||
from CTFd.utils import get_config
 | 
			
		||||
 | 
			
		||||
from .docker import get_docker_client
 | 
			
		||||
from .routers import Router, _routers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WhaleChecks:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def check_docker_api():
 | 
			
		||||
        try:
 | 
			
		||||
            client = get_docker_client()
 | 
			
		||||
        except TLSParameterError as e:
 | 
			
		||||
            return f'Docker TLS Parameters incorrect ({e})'
 | 
			
		||||
        except DockerException as e:
 | 
			
		||||
            return f'Docker API url incorrect ({e})'
 | 
			
		||||
        try:
 | 
			
		||||
            client.ping()
 | 
			
		||||
        except (APIError, requests.RequestException):
 | 
			
		||||
            return f'Unable to connect to Docker API, check your API connectivity'
 | 
			
		||||
 | 
			
		||||
        credentials = get_config("whale:docker_credentials")
 | 
			
		||||
        if credentials and credentials.count(':') == 1:
 | 
			
		||||
            try:
 | 
			
		||||
                client.login(*credentials.split(':'))
 | 
			
		||||
            except DockerException:
 | 
			
		||||
                return f'Unable to log into docker registry, check your credentials'
 | 
			
		||||
        swarm = client.info()['Swarm']
 | 
			
		||||
        if not swarm['ControlAvailable']:
 | 
			
		||||
            return f'Docker swarm not available. You should initialize a swarm first. ($ docker swarm init)'
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def check_frp_connection():
 | 
			
		||||
        router_conftype = get_config("whale:router_type", "frp")
 | 
			
		||||
        if router_conftype not in _routers:
 | 
			
		||||
            return "invalid router type: " + router_conftype
 | 
			
		||||
        ok, msg = _routers[router_conftype]().check_availability()
 | 
			
		||||
        if not ok:
 | 
			
		||||
            return msg
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def perform():
 | 
			
		||||
        errors = []
 | 
			
		||||
        for attr in dir(WhaleChecks):
 | 
			
		||||
            if attr.startswith('check_'):
 | 
			
		||||
                err = getattr(WhaleChecks, attr)()
 | 
			
		||||
                if err:
 | 
			
		||||
                    errors.append(err)
 | 
			
		||||
        return errors
 | 
			
		||||
							
								
								
									
										61
									
								
								utils/control.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								utils/control.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from CTFd.utils import get_config
 | 
			
		||||
from .db import DBContainer, db
 | 
			
		||||
from .docker import DockerUtils
 | 
			
		||||
from .routers import Router
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ControlUtil:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def try_add_container(user_id, challenge_id):
 | 
			
		||||
        container = DBContainer.create_container_record(user_id, challenge_id)
 | 
			
		||||
        try:
 | 
			
		||||
            DockerUtils.add_container(container)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            DBContainer.remove_container_record(user_id)
 | 
			
		||||
            print(traceback.format_exc())
 | 
			
		||||
            return False, 'Docker Creation Error'
 | 
			
		||||
        ok, msg = Router.register(container)
 | 
			
		||||
        if not ok:
 | 
			
		||||
            DockerUtils.remove_container(container)
 | 
			
		||||
            DBContainer.remove_container_record(user_id)
 | 
			
		||||
            return False, msg
 | 
			
		||||
        return True, 'Container created'
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def try_remove_container(user_id):
 | 
			
		||||
        container = DBContainer.get_current_containers(user_id=user_id)
 | 
			
		||||
        if not container:
 | 
			
		||||
            return False, 'No such container'
 | 
			
		||||
        for _ in range(3):  # configurable? as "onerror_retry_cnt"
 | 
			
		||||
            try:
 | 
			
		||||
                ok, msg = Router.unregister(container)
 | 
			
		||||
                if not ok:
 | 
			
		||||
                    return False, msg
 | 
			
		||||
                DockerUtils.remove_container(container)
 | 
			
		||||
                DBContainer.remove_container_record(user_id)
 | 
			
		||||
                return True, 'Container destroyed'
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                print(traceback.format_exc())
 | 
			
		||||
        return False, 'Failed when destroying instance, please contact admin!'
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def try_renew_container(user_id):
 | 
			
		||||
        container = DBContainer.get_current_containers(user_id)
 | 
			
		||||
        if not container:
 | 
			
		||||
            return False, 'No such container'
 | 
			
		||||
        timeout = int(get_config("whale:docker_timeout", "3600"))
 | 
			
		||||
        container.start_time = container.start_time + \
 | 
			
		||||
                               datetime.timedelta(seconds=timeout)
 | 
			
		||||
        if container.start_time > datetime.datetime.now():
 | 
			
		||||
            container.start_time = datetime.datetime.now()
 | 
			
		||||
            # race condition? useless maybe?
 | 
			
		||||
            # useful when docker_timeout < poll timeout (10 seconds)
 | 
			
		||||
            # doesn't make any sense
 | 
			
		||||
        else:
 | 
			
		||||
            return False, 'Invalid container'
 | 
			
		||||
        container.renew_count += 1
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
        return True, 'Container Renewed'
 | 
			
		||||
							
								
								
									
										104
									
								
								utils/db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								utils/db.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from CTFd.models import db
 | 
			
		||||
from CTFd.utils import get_config
 | 
			
		||||
from ..models import WhaleContainer, WhaleRedirectTemplate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DBContainer:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_container_record(user_id, challenge_id):
 | 
			
		||||
        container = WhaleContainer(user_id=user_id, challenge_id=challenge_id)
 | 
			
		||||
        db.session.add(container)
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
 | 
			
		||||
        return container
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_current_containers(user_id):
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        q = q.filter(WhaleContainer.user_id == user_id)
 | 
			
		||||
        return q.first()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_container_by_port(port):
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        q = q.filter(WhaleContainer.port == port)
 | 
			
		||||
        return q.first()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def remove_container_record(user_id):
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        q = q.filter(WhaleContainer.user_id == user_id)
 | 
			
		||||
        q.delete()
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_expired_container():
 | 
			
		||||
        timeout = int(get_config("whale:docker_timeout", "3600"))
 | 
			
		||||
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        q = q.filter(
 | 
			
		||||
            WhaleContainer.start_time <
 | 
			
		||||
            datetime.datetime.now() - datetime.timedelta(seconds=timeout)
 | 
			
		||||
        )
 | 
			
		||||
        return q.all()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_alive_container():
 | 
			
		||||
        timeout = int(get_config("whale:docker_timeout", "3600"))
 | 
			
		||||
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        q = q.filter(
 | 
			
		||||
            WhaleContainer.start_time >=
 | 
			
		||||
            datetime.datetime.now() - datetime.timedelta(seconds=timeout)
 | 
			
		||||
        )
 | 
			
		||||
        return q.all()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_container():
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        return q.all()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_alive_container_page(page_start, page_end):
 | 
			
		||||
        timeout = int(get_config("whale:docker_timeout", "3600"))
 | 
			
		||||
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        q = q.filter(
 | 
			
		||||
            WhaleContainer.start_time >=
 | 
			
		||||
            datetime.datetime.now() - datetime.timedelta(seconds=timeout)
 | 
			
		||||
        )
 | 
			
		||||
        q = q.slice(page_start, page_end)
 | 
			
		||||
        return q.all()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_alive_container_count():
 | 
			
		||||
        timeout = int(get_config("whale:docker_timeout", "3600"))
 | 
			
		||||
 | 
			
		||||
        q = db.session.query(WhaleContainer)
 | 
			
		||||
        q = q.filter(
 | 
			
		||||
            WhaleContainer.start_time >=
 | 
			
		||||
            datetime.datetime.now() - datetime.timedelta(seconds=timeout)
 | 
			
		||||
        )
 | 
			
		||||
        return q.count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DBRedirectTemplate:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_templates():
 | 
			
		||||
        return WhaleRedirectTemplate.query.all()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_template(name, access_template, frp_template):
 | 
			
		||||
        if WhaleRedirectTemplate.query.filter_by(key=name).first():
 | 
			
		||||
            return  # already existed
 | 
			
		||||
        db.session.add(WhaleRedirectTemplate(
 | 
			
		||||
            name, access_template, frp_template
 | 
			
		||||
        ))
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def delete_template(name):
 | 
			
		||||
        WhaleRedirectTemplate.query.filter_by(key=name).delete()
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
							
								
								
									
										202
									
								
								utils/docker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								utils/docker.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,202 @@
 | 
			
		||||
import json
 | 
			
		||||
import random
 | 
			
		||||
import uuid
 | 
			
		||||
from collections import OrderedDict
 | 
			
		||||
 | 
			
		||||
import docker
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 | 
			
		||||
from CTFd.utils import get_config
 | 
			
		||||
 | 
			
		||||
from .cache import CacheProvider
 | 
			
		||||
from .exceptions import WhaleError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_docker_client():
 | 
			
		||||
    if get_config("whale:docker_use_ssl", False):
 | 
			
		||||
        tls_config = docker.tls.TLSConfig(
 | 
			
		||||
            verify=True,
 | 
			
		||||
            ca_cert=get_config("whale:docker_ssl_ca_cert") or None,
 | 
			
		||||
            client_cert=(
 | 
			
		||||
                get_config("whale:docker_ssl_client_cert"),
 | 
			
		||||
                get_config("whale:docker_ssl_client_key")
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        return docker.DockerClient(
 | 
			
		||||
            base_url=get_config("whale:docker_api_url"),
 | 
			
		||||
            tls=tls_config,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        return docker.DockerClient(base_url=get_config("whale:docker_api_url"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerUtils:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def init():
 | 
			
		||||
        try:
 | 
			
		||||
            DockerUtils.client = get_docker_client()
 | 
			
		||||
            # docker-py is thread safe: https://github.com/docker/docker-py/issues/619
 | 
			
		||||
        except Exception:
 | 
			
		||||
            raise WhaleError(
 | 
			
		||||
                'Docker Connection Error\n'
 | 
			
		||||
                'Please ensure the docker api url (first config item) is correct\n'
 | 
			
		||||
                'if you are using unix:///var/run/docker.sock, check if the socket is correctly mapped'
 | 
			
		||||
            )
 | 
			
		||||
        credentials = get_config("whale:docker_credentials")
 | 
			
		||||
        if credentials and credentials.count(':') == 1:
 | 
			
		||||
            try:
 | 
			
		||||
                DockerUtils.client.login(*credentials.split(':'))
 | 
			
		||||
            except Exception:
 | 
			
		||||
                raise WhaleError('docker.io failed to login, check your credentials')
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def add_container(container):
 | 
			
		||||
        if container.challenge.docker_image.startswith("{"):
 | 
			
		||||
            DockerUtils._create_grouped_container(DockerUtils.client, container)
 | 
			
		||||
        else:
 | 
			
		||||
            DockerUtils._create_standalone_container(DockerUtils.client, container)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _create_standalone_container(client, container):
 | 
			
		||||
        dns = get_config("whale:docker_dns", "").split(",")
 | 
			
		||||
        node = DockerUtils.choose_node(
 | 
			
		||||
            container.challenge.docker_image,
 | 
			
		||||
            get_config("whale:docker_swarm_nodes", "").split(",")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        client.services.create(
 | 
			
		||||
            image=container.challenge.docker_image,
 | 
			
		||||
            name=f'{container.user_id}-{container.uuid}',
 | 
			
		||||
            env={'FLAG': container.flag}, dns_config=docker.types.DNSConfig(nameservers=dns),
 | 
			
		||||
            networks=[get_config("whale:docker_auto_connect_network", "ctfd_frp-containers")],
 | 
			
		||||
            resources=docker.types.Resources(
 | 
			
		||||
                mem_limit=DockerUtils.convert_readable_text(
 | 
			
		||||
                    container.challenge.memory_limit),
 | 
			
		||||
                cpu_limit=int(container.challenge.cpu_limit * 1e9)
 | 
			
		||||
            ),
 | 
			
		||||
            labels={
 | 
			
		||||
                'whale_id': f'{container.user_id}-{container.uuid}'
 | 
			
		||||
            },  # for container deletion
 | 
			
		||||
            constraints=['node.labels.name==' + node],
 | 
			
		||||
            endpoint_spec=docker.types.EndpointSpec(mode='dnsrr', ports={})
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _create_grouped_container(client, container):
 | 
			
		||||
        range_prefix = CacheProvider(app=current_app).get_available_network_range()
 | 
			
		||||
 | 
			
		||||
        ipam_pool = docker.types.IPAMPool(subnet=range_prefix)
 | 
			
		||||
        ipam_config = docker.types.IPAMConfig(
 | 
			
		||||
            driver='default', pool_configs=[ipam_pool])
 | 
			
		||||
        network_name = f'{container.user_id}-{container.uuid}'
 | 
			
		||||
        network = client.networks.create(
 | 
			
		||||
            network_name, internal=True,
 | 
			
		||||
            ipam=ipam_config, attachable=True,
 | 
			
		||||
            labels={'prefix': range_prefix},
 | 
			
		||||
            driver="overlay", scope="swarm"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        dns = []
 | 
			
		||||
        containers = get_config("whale:docker_auto_connect_containers", "").split(",")
 | 
			
		||||
        for c in containers:
 | 
			
		||||
            if not c:
 | 
			
		||||
                continue
 | 
			
		||||
            network.connect(c)
 | 
			
		||||
            if "dns" in c:
 | 
			
		||||
                network.reload()
 | 
			
		||||
                for name in network.attrs['Containers']:
 | 
			
		||||
                    if network.attrs['Containers'][name]['Name'] == c:
 | 
			
		||||
                        dns.append(network.attrs['Containers'][name]['IPv4Address'].split('/')[0])
 | 
			
		||||
 | 
			
		||||
        has_processed_main = False
 | 
			
		||||
        try:
 | 
			
		||||
            images = json.loads(
 | 
			
		||||
                container.challenge.docker_image,
 | 
			
		||||
                object_pairs_hook=OrderedDict
 | 
			
		||||
            )
 | 
			
		||||
        except json.JSONDecodeError:
 | 
			
		||||
            raise WhaleError(
 | 
			
		||||
                "Challenge Image Parse Error\n"
 | 
			
		||||
                "plase check the challenge image string"
 | 
			
		||||
            )
 | 
			
		||||
        for name, image in images.items():
 | 
			
		||||
            if has_processed_main:
 | 
			
		||||
                container_name = f'{container.user_id}-{uuid.uuid4()}'
 | 
			
		||||
            else:
 | 
			
		||||
                container_name = f'{container.user_id}-{container.uuid}'
 | 
			
		||||
                node = DockerUtils.choose_node(image, get_config("whale:docker_swarm_nodes", "").split(","))
 | 
			
		||||
                has_processed_main = True
 | 
			
		||||
            client.services.create(
 | 
			
		||||
                image=image, name=container_name, networks=[
 | 
			
		||||
                    docker.types.NetworkAttachmentConfig(network_name, aliases=[name])
 | 
			
		||||
                ],
 | 
			
		||||
                env={'FLAG': container.flag},
 | 
			
		||||
                dns_config=docker.types.DNSConfig(nameservers=dns),
 | 
			
		||||
                resources=docker.types.Resources(
 | 
			
		||||
                    mem_limit=DockerUtils.convert_readable_text(
 | 
			
		||||
                        container.challenge.memory_limit
 | 
			
		||||
                    ),
 | 
			
		||||
                    cpu_limit=int(container.challenge.cpu_limit * 1e9)),
 | 
			
		||||
                labels={
 | 
			
		||||
                    'whale_id': f'{container.user_id}-{container.uuid}'
 | 
			
		||||
                },  # for container deletion
 | 
			
		||||
                hostname=name, constraints=['node.labels.name==' + node],
 | 
			
		||||
                endpoint_spec=docker.types.EndpointSpec(mode='dnsrr', ports={})
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def remove_container(container):
 | 
			
		||||
        whale_id = f'{container.user_id}-{container.uuid}'
 | 
			
		||||
 | 
			
		||||
        for s in DockerUtils.client.services.list(filters={'label': f'whale_id={whale_id}'}):
 | 
			
		||||
            s.remove()
 | 
			
		||||
 | 
			
		||||
        networks = DockerUtils.client.networks.list(names=[whale_id])
 | 
			
		||||
        if len(networks) > 0:  # is grouped containers
 | 
			
		||||
            auto_containers = get_config("whale:docker_auto_connect_containers", "").split(",")
 | 
			
		||||
            redis_util = CacheProvider(app=current_app)
 | 
			
		||||
            for network in networks:
 | 
			
		||||
                for container in auto_containers:
 | 
			
		||||
                    try:
 | 
			
		||||
                        network.disconnect(container, force=True)
 | 
			
		||||
                    except Exception:
 | 
			
		||||
                        pass
 | 
			
		||||
                redis_util.add_available_network_range(network.attrs['Labels']['prefix'])
 | 
			
		||||
                network.remove()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def convert_readable_text(text):
 | 
			
		||||
        lower_text = text.lower()
 | 
			
		||||
 | 
			
		||||
        if lower_text.endswith("k"):
 | 
			
		||||
            return int(text[:-1]) * 1024
 | 
			
		||||
 | 
			
		||||
        if lower_text.endswith("m"):
 | 
			
		||||
            return int(text[:-1]) * 1024 * 1024
 | 
			
		||||
 | 
			
		||||
        if lower_text.endswith("g"):
 | 
			
		||||
            return int(text[:-1]) * 1024 * 1024 * 1024
 | 
			
		||||
 | 
			
		||||
        return 0
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def choose_node(image, nodes):
 | 
			
		||||
        win_nodes = []
 | 
			
		||||
        linux_nodes = []
 | 
			
		||||
        for node in nodes:
 | 
			
		||||
            if node.startswith("windows"):
 | 
			
		||||
                win_nodes.append(node)
 | 
			
		||||
            else:
 | 
			
		||||
                linux_nodes.append(node)
 | 
			
		||||
        try:
 | 
			
		||||
            tag = image.split(":")[1:]
 | 
			
		||||
            if len(tag) and tag[0].startswith("windows"):
 | 
			
		||||
                return random.choice(win_nodes)
 | 
			
		||||
            return random.choice(linux_nodes)
 | 
			
		||||
        except IndexError:
 | 
			
		||||
            raise WhaleError(
 | 
			
		||||
                'No Suitable Nodes.\n'
 | 
			
		||||
                'If you are using Whale for the first time, \n'
 | 
			
		||||
                'Please Setup Swarm Nodes Correctly and Lable Them with\n'
 | 
			
		||||
                'docker node update --label-add "name=linux-1" $(docker node ls -q)'
 | 
			
		||||
            )
 | 
			
		||||
							
								
								
									
										8
									
								
								utils/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								utils/exceptions.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
class WhaleError(Exception):
 | 
			
		||||
    def __init__(self, msg):
 | 
			
		||||
        super().__init__(msg)
 | 
			
		||||
        self.message = msg
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WhaleWarning(Warning):
 | 
			
		||||
    pass
 | 
			
		||||
							
								
								
									
										34
									
								
								utils/routers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								utils/routers/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
from CTFd.utils import get_config
 | 
			
		||||
 | 
			
		||||
from .frp import FrpRouter
 | 
			
		||||
from .trp import TrpRouter
 | 
			
		||||
 | 
			
		||||
_routers = {
 | 
			
		||||
    'frp': FrpRouter,
 | 
			
		||||
    'trp': TrpRouter,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def instanciate(cls):
 | 
			
		||||
    return cls()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@instanciate
 | 
			
		||||
class Router:
 | 
			
		||||
    _name = ''
 | 
			
		||||
    _router = None
 | 
			
		||||
 | 
			
		||||
    def __getattr__(self, name: str):
 | 
			
		||||
        router_conftype = get_config("whale:router_type", "frp")
 | 
			
		||||
        if Router._name != router_conftype:
 | 
			
		||||
            Router._router = _routers[router_conftype]()
 | 
			
		||||
            Router._name = router_conftype
 | 
			
		||||
        return getattr(Router._router, name)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def reset():
 | 
			
		||||
        Router._name = ''
 | 
			
		||||
        Router._router = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ["Router"]
 | 
			
		||||
							
								
								
									
										25
									
								
								utils/routers/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								utils/routers/base.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
import typing
 | 
			
		||||
 | 
			
		||||
from ...models import WhaleContainer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseRouter:
 | 
			
		||||
    name = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def access(self, container: WhaleContainer):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def register(self, container: WhaleContainer):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def unregister(self, container: WhaleContainer):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def reload(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def check_availability(self) -> typing.Tuple[bool, str]:
 | 
			
		||||
        pass
 | 
			
		||||
							
								
								
									
										132
									
								
								utils/routers/frp.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								utils/routers/frp.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,132 @@
 | 
			
		||||
import warnings
 | 
			
		||||
 | 
			
		||||
from flask import current_app
 | 
			
		||||
from requests import session, RequestException
 | 
			
		||||
 | 
			
		||||
from CTFd.models import db
 | 
			
		||||
from CTFd.utils import get_config, set_config, logging
 | 
			
		||||
 | 
			
		||||
from .base import BaseRouter
 | 
			
		||||
from ..cache import CacheProvider
 | 
			
		||||
from ..db import DBContainer
 | 
			
		||||
from ..exceptions import WhaleError, WhaleWarning
 | 
			
		||||
from ...models import WhaleContainer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FrpRouter(BaseRouter):
 | 
			
		||||
    name = "frp"
 | 
			
		||||
    types = {
 | 
			
		||||
        'direct': 'tcp',
 | 
			
		||||
        'http': 'http',
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class FrpRule:
 | 
			
		||||
        def __init__(self, name, config):
 | 
			
		||||
            self.name = name
 | 
			
		||||
            self.config = config
 | 
			
		||||
 | 
			
		||||
        def __str__(self) -> str:
 | 
			
		||||
            return f'[{self.name}]\n' + '\n'.join(f'{k} = {v}' for k, v in self.config.items())
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.ses = session()
 | 
			
		||||
        self.url = get_config("whale:frp_api_url").rstrip("/")
 | 
			
		||||
        self.common = ''
 | 
			
		||||
        try:
 | 
			
		||||
            CacheProvider(app=current_app).init_port_sets()
 | 
			
		||||
        except Exception:
 | 
			
		||||
            warnings.warn(
 | 
			
		||||
                "cache initialization failed",
 | 
			
		||||
                WhaleWarning
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def reload(self, exclude=None):
 | 
			
		||||
        rules = []
 | 
			
		||||
        for container in DBContainer.get_all_alive_container():
 | 
			
		||||
            if container.uuid == exclude:
 | 
			
		||||
                continue
 | 
			
		||||
            name = f'{container.challenge.redirect_type}_{container.user_id}_{container.uuid}'
 | 
			
		||||
            config = {
 | 
			
		||||
                'type': self.types[container.challenge.redirect_type],
 | 
			
		||||
                'local_ip': f'{container.user_id}-{container.uuid}',
 | 
			
		||||
                'local_port': container.challenge.redirect_port,
 | 
			
		||||
                'use_compression': 'true',
 | 
			
		||||
            }
 | 
			
		||||
            if config['type'] == 'http':
 | 
			
		||||
                config['subdomain'] = container.http_subdomain
 | 
			
		||||
            elif config['type'] == 'tcp':
 | 
			
		||||
                config['remote_port'] = container.port
 | 
			
		||||
            rules.append(self.FrpRule(name, config))
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.common:
 | 
			
		||||
                common = get_config("whale:frp_config_template", '')
 | 
			
		||||
                if '[common]' in common:
 | 
			
		||||
                    self.common = common
 | 
			
		||||
                else:
 | 
			
		||||
                    remote = self.ses.get(f'{self.url}/api/config')
 | 
			
		||||
                    assert remote.status_code == 200
 | 
			
		||||
                    set_config("whale:frp_config_template", remote.text)
 | 
			
		||||
                    self.common = remote.text
 | 
			
		||||
            config = self.common + '\n' + '\n'.join(str(r) for r in rules)
 | 
			
		||||
            assert self.ses.put(
 | 
			
		||||
                f'{self.url}/api/config', config, timeout=5
 | 
			
		||||
            ).status_code == 200
 | 
			
		||||
            assert self.ses.get(
 | 
			
		||||
                f'{self.url}/api/reload', timeout=5
 | 
			
		||||
            ).status_code == 200
 | 
			
		||||
        except (RequestException, AssertionError) as e:
 | 
			
		||||
            raise WhaleError(
 | 
			
		||||
                '\nfrpc request failed\n' +
 | 
			
		||||
                (f'{e}\n' if str(e) else '') +
 | 
			
		||||
                'please check the frp related configs'
 | 
			
		||||
            ) from None
 | 
			
		||||
 | 
			
		||||
    def access(self, container: WhaleContainer):
 | 
			
		||||
        if container.challenge.redirect_type == 'direct':
 | 
			
		||||
            return f'nc {get_config("whale:frp_direct_ip_address", "127.0.0.1")} {container.port}'
 | 
			
		||||
        elif container.challenge.redirect_type == 'http':
 | 
			
		||||
            host = get_config("whale:frp_http_domain_suffix", "")
 | 
			
		||||
            port = get_config("whale:frp_http_port", "80")
 | 
			
		||||
            host += f':{port}' if port != 80 else ''
 | 
			
		||||
            return f'<a target="_blank" href="http://{container.http_subdomain}.{host}/">Link to the Challenge</a>'
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
    def register(self, container: WhaleContainer):
 | 
			
		||||
        if container.challenge.redirect_type == 'direct':
 | 
			
		||||
            if not container.port:
 | 
			
		||||
                port = CacheProvider(app=current_app).get_available_port()
 | 
			
		||||
                if not port:
 | 
			
		||||
                    return False, 'No available ports. Please wait for a few minutes.'
 | 
			
		||||
                container.port = port
 | 
			
		||||
                db.session.commit()
 | 
			
		||||
        elif container.challenge.redirect_type == 'http':
 | 
			
		||||
            # config['subdomain'] = container.http_subdomain
 | 
			
		||||
            pass
 | 
			
		||||
        self.reload()
 | 
			
		||||
        return True, 'success'
 | 
			
		||||
 | 
			
		||||
    def unregister(self, container: WhaleContainer):
 | 
			
		||||
        if container.challenge.redirect_type == 'direct':
 | 
			
		||||
            try:
 | 
			
		||||
                redis_util = CacheProvider(app=current_app)
 | 
			
		||||
                redis_util.add_available_port(container.port)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logging.log(
 | 
			
		||||
                    'whale', 'Error deleting port from cache',
 | 
			
		||||
                    name=container.user.name,
 | 
			
		||||
                    challenge_id=container.challenge_id,
 | 
			
		||||
                )
 | 
			
		||||
                return False, 'Error deleting port from cache'
 | 
			
		||||
        self.reload(exclude=container.uuid)
 | 
			
		||||
        return True, 'success'
 | 
			
		||||
 | 
			
		||||
    def check_availability(self):
 | 
			
		||||
        try:
 | 
			
		||||
            resp = self.ses.get(f'{self.url}/api/status', timeout=2.0)
 | 
			
		||||
        except RequestException as e:
 | 
			
		||||
            return False, 'Unable to access frpc admin api'
 | 
			
		||||
        if resp.status_code == 401:
 | 
			
		||||
            return False, 'frpc admin api unauthorized'
 | 
			
		||||
        return True, 'Available'
 | 
			
		||||
							
								
								
									
										69
									
								
								utils/routers/trp.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								utils/routers/trp.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
			
		||||
import traceback
 | 
			
		||||
from requests import session, RequestException, HTTPError
 | 
			
		||||
 | 
			
		||||
from CTFd.utils import get_config
 | 
			
		||||
from .base import BaseRouter
 | 
			
		||||
from ..db import DBContainer, WhaleContainer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrpRouter(BaseRouter):
 | 
			
		||||
    name = "trp"
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.ses = session()
 | 
			
		||||
        self.url = get_config('whale:trp_api_url', '').rstrip("/")
 | 
			
		||||
        self.common = ''
 | 
			
		||||
        for container in DBContainer.get_all_alive_container():
 | 
			
		||||
            self.register(container)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_domain(container: WhaleContainer):
 | 
			
		||||
        domain = get_config('whale:trp_domain_suffix', '127.0.0.1.nip.io').lstrip('.')
 | 
			
		||||
        domain = f'{container.uuid}.{domain}'
 | 
			
		||||
        return domain
 | 
			
		||||
 | 
			
		||||
    def access(self, container: WhaleContainer):
 | 
			
		||||
        ch_type = container.challenge.redirect_type
 | 
			
		||||
        domain = self.get_domain(container)
 | 
			
		||||
        port = get_config('whale:trp_listening_port', 1443)
 | 
			
		||||
        if ch_type == 'direct':
 | 
			
		||||
            return f'from pwn import *<br>remote("{domain}", {port}, ssl=True).interactive()'
 | 
			
		||||
        elif ch_type == 'http':
 | 
			
		||||
            return f'https://{domain}' + (f':{port}' if port != 443 else '')
 | 
			
		||||
        else:
 | 
			
		||||
            return f'[ssl] {domain} {port}'
 | 
			
		||||
 | 
			
		||||
    def register(self, container: WhaleContainer):
 | 
			
		||||
        try:
 | 
			
		||||
            resp = self.ses.post(f'{self.url}/rule/{self.get_domain(container)}', json={
 | 
			
		||||
                'target': f'{container.user_id}-{container.uuid}:{container.challenge.redirect_port}',
 | 
			
		||||
                'source': None,
 | 
			
		||||
            })
 | 
			
		||||
            resp.raise_for_status()
 | 
			
		||||
            return True, 'success'
 | 
			
		||||
        except HTTPError as e:
 | 
			
		||||
            return False, e.response.text
 | 
			
		||||
        except RequestException as e:
 | 
			
		||||
            print(traceback.format_exc())
 | 
			
		||||
            return False, 'unable to access trp Api'
 | 
			
		||||
 | 
			
		||||
    def unregister(self, container: WhaleContainer):
 | 
			
		||||
        try:
 | 
			
		||||
            resp = self.ses.delete(f'{self.url}/rule/{self.get_domain(container)}')
 | 
			
		||||
            resp.raise_for_status()
 | 
			
		||||
            return True, 'success'
 | 
			
		||||
        except HTTPError as e:
 | 
			
		||||
            return False, e.response.text
 | 
			
		||||
        except RequestException as e:
 | 
			
		||||
            print(traceback.format_exc())
 | 
			
		||||
            return False, 'unable to access trp Api'
 | 
			
		||||
 | 
			
		||||
    def check_availability(self):
 | 
			
		||||
        try:
 | 
			
		||||
            resp = self.ses.get(f'{self.url}/rules').json()
 | 
			
		||||
        except RequestException as e:
 | 
			
		||||
            return False, 'Unable to access trp admin api'
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return False, 'Unknown trp error'
 | 
			
		||||
        return True, 'Available'
 | 
			
		||||
							
								
								
									
										60
									
								
								utils/setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								utils/setup.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
from CTFd.utils import set_config
 | 
			
		||||
 | 
			
		||||
from ..models import WhaleRedirectTemplate, db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup_default_configs():
 | 
			
		||||
    for key, val in {
 | 
			
		||||
        'setup': 'true',
 | 
			
		||||
        'docker_api_url': 'unix:///var/run/docker.sock',
 | 
			
		||||
        'docker_credentials': '',
 | 
			
		||||
        'docker_dns': '127.0.0.1',
 | 
			
		||||
        'docker_max_container_count': '100',
 | 
			
		||||
        'docker_max_renew_count': '5',
 | 
			
		||||
        'docker_subnet': '174.1.0.0/16',
 | 
			
		||||
        'docker_subnet_new_prefix': '24',
 | 
			
		||||
        'docker_swarm_nodes': 'linux-1',
 | 
			
		||||
        'docker_timeout': '3600',
 | 
			
		||||
        'frp_api_url': 'http://frpc:7400',
 | 
			
		||||
        'frp_http_port': '8080',
 | 
			
		||||
        'frp_http_domain_suffix': '127.0.0.1.nip.io',
 | 
			
		||||
        'frp_direct_port_maximum': '10100',
 | 
			
		||||
        'frp_direct_port_minimum': '10000',
 | 
			
		||||
        'template_http_subdomain': '{{ container.uuid }}',
 | 
			
		||||
        'template_chall_flag': '{{ "flag{"+uuid.uuid4()|string+"}" }}',
 | 
			
		||||
    }.items():
 | 
			
		||||
        set_config('whale:' + key, val)
 | 
			
		||||
    db.session.add(WhaleRedirectTemplate(
 | 
			
		||||
        'http',
 | 
			
		||||
        'http://{{ container.http_subdomain }}.'
 | 
			
		||||
        '{{ get_config("whale:frp_http_domain_suffix", "") }}'
 | 
			
		||||
        '{% if get_config("whale:frp_http_port", "80") != 80 %}:{{ get_config("whale:frp_http_port") }}{% endif %}/',
 | 
			
		||||
        '''
 | 
			
		||||
[http_{{ container.user_id|string }}-{{ container.uuid }}]
 | 
			
		||||
type = http
 | 
			
		||||
local_ip = {{ container.user_id|string }}-{{ container.uuid }}
 | 
			
		||||
local_port = {{ container.challenge.redirect_port }}
 | 
			
		||||
subdomain = {{ container.http_subdomain }}
 | 
			
		||||
use_compression = true
 | 
			
		||||
'''
 | 
			
		||||
    ))
 | 
			
		||||
    db.session.add(WhaleRedirectTemplate(
 | 
			
		||||
        'direct',
 | 
			
		||||
        'nc {{ get_config("whale:frp_direct_ip_address", "127.0.0.1") }} {{ container.port }}',
 | 
			
		||||
        '''
 | 
			
		||||
[direct_{{ container.user_id|string }}-{{ container.uuid }}]
 | 
			
		||||
type = tcp
 | 
			
		||||
local_ip = {{ container.user_id|string }}-{{ container.uuid }}
 | 
			
		||||
local_port = {{ container.challenge.redirect_port }}
 | 
			
		||||
remote_port = {{ container.port }}
 | 
			
		||||
use_compression = true
 | 
			
		||||
 | 
			
		||||
[direct_{{ container.user_id|string }}-{{ container.uuid }}_udp]
 | 
			
		||||
type = udp
 | 
			
		||||
local_ip = {{ container.user_id|string }}-{{ container.uuid }}
 | 
			
		||||
local_port = {{ container.challenge.redirect_port }}
 | 
			
		||||
remote_port = {{ container.port }}
 | 
			
		||||
use_compression = true
 | 
			
		||||
'''
 | 
			
		||||
    ))
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
		Reference in New Issue
	
	Block a user