diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..b7ffc2a --- /dev/null +++ b/.drone.yml @@ -0,0 +1,26 @@ +kind: pipeline +type: docker +name: Docker image build + +trigger: + branch: + - master + + +steps: + - name: config + image: alpine + commands: + - echo -n "$VERSION,latest" > .tags + - sed -i -E "s/var_FRP_VERSION/$FRP_VERSION/" Dockerfile + - apk add openssl + - ./gen_uploads.sh + + - name: build + image: plugins/docker + settings: + username: + from_secret: docker_username + password: + from_secret: docker_password + repo: quackerd/rainloop diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5c8df0b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM alpine:latest + + +ENV VERSION=var_VERSION +ENV URL https://github.com/XTLS/Xray-core/releases/download/v${VERSION}/Xray-linux-64.zip + +COPY ./run.sh /opt/run.sh + +RUN set -xe && \ + apk add --no-cache unzip wget nginx certbot openssl && \ + wget ${URL} && \ + mkdir -p /opt/xray && \ + unzip Xray-linux-64.zip -d /opt/xray && \ + rm Xray-linux-64.zip && \ + mkdir -p /opt/config && \ + mkdir -p /opt/config/logs && \ + mkdir -p /opt/config/certs && \ + mkdir -p /opt/config/logs/nginx && \ + mkdir -p /opt/config/logs/xray && \ + mkdir -p /opt/config/logs/crond && \ + chmod +x /opt/run.sh && \ + apk del unzip wget + +COPY ./nginx.conf /opt/nginx.conf +COPY ./crontab /var/spool/cron/crontabs/root + +EXPOSE 80 +EXPOSE 443 + +CMD ["/opt/run.sh"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 2ec0089..0000000 --- a/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -MIT License Copyright (c) 2021 quackerd - -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 (including the next -paragraph) 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. diff --git a/client_conf.in b/client_conf.in deleted file mode 100644 index 43e8098..0000000 --- a/client_conf.in +++ /dev/null @@ -1,118 +0,0 @@ -{ - "dns": { - "servers": [ - "223.5.5.5", - "114.114.114.114", - { - "address": "8.8.8.8", - "port": 53, - "domains": [ - "geosite:geolocation-!cn" - ] - }, - { - "address": "1.1.1.1", - "port": 53, - "domains": [ - "geosite:geolocation-!cn" - ] - } - ] - }, - - "routing": { - "domainStrategy": "IPIfNonMatch", - "rules": [ - { - "type": "field", - "outboundTag": "direct", - "ip": [ - "223.5.5.5", - "114.114.114.114" - ] - }, - { - "type": "field", - "outboundTag": "proxy", - "ip": [ - "8.8.8.8", - "1.1.1.1" - ] - }, - { - "type": "field", - "outboundTag": "direct", - "ip": [ - "geoip:cn", - "geoip:private" - ] - }, - { - "type": "field", - "outboundTag": "direct", - "domain": ["geosite:cn"] - }, - { - "type": "field", - "outboundTag": "proxy", - "network": "udp,tcp" - } - ] - }, - - "inbounds": [ - { - "port": {{ port }}, - "listen": "127.0.0.1", - "protocol": "socks", - "sniffing": { - "enabled": true, - "destOverride": ["http", "tls"] - }, - "settings": { - "auth": "noauth", - "udp": false - } - } - ], - "outbounds": [ - { - "tag": "proxy", - "protocol": "vless", - "settings": { - "vnext": [ - { - "address": "{{ subdomain }}.{{ domain }}", - "port": 443, - "users": [ - { - "id": "{{ id }}", - "encryption": "none", - "level": 0 - } - ] - } - ] - }, - "streamSettings": { - "network": "tcp", - "security": "xtls", - "xtlsSettings": { - "serverName": "{{ subdomain }}.{{ domain }}", - "allowInsecure": false, - "alpn": ["h2","http/1.1"] - } - } - }, - { - "protocol": "freedom", - "settings": {}, - "tag": "direct" - }, - { - "protocol": "blackhole", - "settings": {}, - "tag": "block" - } - ] -} \ No newline at end of file diff --git a/client_obj.in b/client_obj.in deleted file mode 100644 index 4d1bb6f..0000000 --- a/client_obj.in +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "{{ id }}", - "flow": "{{ flow }}", - "level": 0 -} \ No newline at end of file diff --git a/config.yml b/config.yml deleted file mode 100644 index 91cb472..0000000 --- a/config.yml +++ /dev/null @@ -1,43 +0,0 @@ -server: - # domain: the domain name, e.g. google.com - domain: domain.tld - # subdomain: the subdomain name, e.g. write vps for vps.google.com. leave empty for naked domain. - subdomain: example - # loglevel: the log level of xray - # default: warning - loglevel: - # email: your email for the registered SSL cert. leave empty for a dummy email. - # default: dummy@dummy.org - email: - # uid/gid: the user/group to run the docker-compose stack. - # default: the current user - # comment: you can also manually set, e.g. uid: 1000 gid: 1000 - uid: - gid: - # watchtower: enable watchtower docker image auto-update - # default: False - watchtower: False - -clients: - # name: the name of the client, cannot be null - - name: example_user1 - # id: the password(uuid) of each user, this can either be a string or an UUID. Please read the "comment" and "IMPORTANT" sections. - # default: auto-generated random string - # comment: for a managed environment we recommend hardcoding the id - # the generated id does NOT currently back-propagate to this file - # you WILL lose existing users if you run configure.py multiple times with blank ids as they will be regenerated with random ids - # - # ! IMPORTANT !: id DOES NOT have to be UUID, can be any string of length 1-30. This is supported by xray but not by some v2ray clients - # if the script detects that the id is not a valid UUID, it will also output the equivalent UUID of that string - id: - # flow: the flow parameter - # default: xtls-rprx-direct - flow: - # port: local socks5 proxy port on clients' machines - # default: 1080 - port: - # you can also set each field manually like below: - # - name: example_user2 - # id: example_passwd2 - # flow: xtls-rprx-direct - # port: 6666 diff --git a/configure.py b/configure.py deleted file mode 100644 index 080f6ca..0000000 --- a/configure.py +++ /dev/null @@ -1,188 +0,0 @@ -import getopt -import sys -import uuid -import pwd -import jinja2 -import random -import os -import string -import yaml - -OUTPUT_DIR = "build" -CLIENT_OUTPUT_DIR = "clients" -DEFAULT_EMAIL = "dummy@dummy.org" -CONF_FILE = "config.yml" -SERVER_IN = "server.in" -SERVER_PATH = "xray" -SERVER_FN = "config.json" -NGINX_IN = "nginx.in" -NGINX_PATH = "nginx/nginx/site-confs" -NGINX_FN = "default" -CLIENT_OBJ_IN = "client_obj.in" -CLIENT_CONF_IN = "client_conf.in" -DOCKER_IN = "docker-compose.in" -WATCHTOWER_IN = "watchtower.in" -DEFAULT_WATCHTOWER_ENABLE = False -DEFAULT_CLIENT_PORT = 1080 -DEFAULT_USER_FLOW = "xtls-rprx-direct" -DEFAULT_LOGLEVEL= "warning" -UUID_NAMESPACE = uuid.UUID('00000000-0000-0000-0000-000000000000') - -def calc_uuid5(val): - return str(uuid.uuid5(UUID_NAMESPACE, val)) - -def is_valid_uuid(val): - try: - uuid.UUID(str(val)) - return True - except ValueError: - return False - -def yaml_key_exists_else(mapping : [], name : str, other_val = None, nullable = True): - if (name in mapping) and (mapping[name] != None): - return mapping[name] - else: - if not nullable: - raise Exception("Key " + name + " must not be null.") - else: - return other_val - -def random_string(stringLength=16): - letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(stringLength)) - -class Client: - def __init__(self, conf_obj): - self.name = str(yaml_key_exists_else(conf_obj, 'name', nullable=False)) - self.id = str(yaml_key_exists_else(conf_obj, 'id', other_val=random_string())) - self.flow = str(yaml_key_exists_else(conf_obj, 'flow', other_val=DEFAULT_USER_FLOW)) - self.port = int(yaml_key_exists_else(conf_obj, 'port', other_val=DEFAULT_CLIENT_PORT)) - - def print(self, ident): - pre = "" - for i in range(ident): - pre += " " - - print(pre + "{") - print(pre + " id: " + self.id) - print(pre + " uuid: " + (self.id if is_valid_uuid(self.id) else calc_uuid5(self.id))) - print(pre + " flow: " + self.flow) - print(pre + " port: " + str(self.port)) - print(pre + "}") - - - -class Config: - def print(self): - print("Server configuration:") - print(" domain: " + self.domain) - print(" email: " + self.email) - print(" subdomain: " + self.subdomain) - print(" uid: " + str(self.uid)) - print(" gid: " + str(self.gid)) - print(" loglevel: " + self.loglevel) - print(" clients:") - for i in range(len(self.clients)): - self.clients[i].print(8) - - def __init__(self, f): - conf = yaml.safe_load(f) - conf_srv = conf['server'] - - self.domain = str(yaml_key_exists_else(conf_srv, 'domain', nullable=False)) - self.email = str(yaml_key_exists_else(conf_srv, 'email', other_val=DEFAULT_EMAIL)) - self.subdomain = str(yaml_key_exists_else(conf_srv, 'subdomain', other_val="")) - self.subdomain_only = len(self.subdomain) > 0 - self.uid = int(yaml_key_exists_else(conf_srv, 'uid', other_val=os.getuid())) - self.gid = int(yaml_key_exists_else(conf_srv, 'gid', other_val=os.getgid())) - self.loglevel = str(yaml_key_exists_else(conf_srv, 'loglevel', other_val=DEFAULT_LOGLEVEL)) - self.watchtower = bool(yaml_key_exists_else(conf_srv, 'watchtower', other_val=DEFAULT_WATCHTOWER_ENABLE)) - - self.clients = [] - conf_clients = conf['clients'] - for i in range(len(conf_clients)): - self.clients.append(Client(conf_clients[i])) - -def main(): - with open(CONF_FILE, 'r') as f: - conf = Config(f) - conf.print() - - template_dict = {} - template_dict['uid'] = conf.uid - template_dict['gid'] = conf.gid - template_dict['subdomain'] = conf.subdomain - template_dict['subdomain_only'] = str(conf.subdomain_only).lower() - template_dict['domain'] = conf.domain - template_dict['email'] = conf.email - template_dict['loglevel'] = conf.loglevel - - if conf.watchtower: - with open(WATCHTOWER_IN, "r") as f: - template_dict['watchtower'] = f.read() - else: - template_dict['watchtower'] = "" - - clients = "" - with open(CLIENT_OBJ_IN, "r") as f: - client_obj = f.read() - for i in range(len(conf.clients)): - if i > 0: - clients += ",\n" - clients += jinja2.Template(client_obj).render(id = conf.clients[i].id, - flow = conf.clients[i].flow) - template_dict['clients'] = clients - - print("Generating files...") - - # create output dir - os.makedirs(OUTPUT_DIR, exist_ok=True) - - # generate docker-compose.yml - path = os.path.join(OUTPUT_DIR, "docker-compose.yml") - with open(DOCKER_IN, "r") as f: - template = jinja2.Template(f.read()) - with open(path, "w") as f: - f.write(template.render(template_dict)) - - # generate NGINX conf - with open(NGINX_IN, "r") as f: - template = jinja2.Template(f.read()) - path = os.path.join(OUTPUT_DIR, NGINX_PATH) - os.makedirs(path, exist_ok=True) - path = os.path.join(path, NGINX_FN) - with open(path, "w") as f: - f.write(template.render(template_dict)) - - # generate xray conf - with open(SERVER_IN, "r") as f: - template = jinja2.Template(f.read()) - path = os.path.join(OUTPUT_DIR, SERVER_PATH) - os.makedirs(path, exist_ok=True) - path = os.path.join(path, SERVER_FN) - with open(path, "w") as f: - f.write(template.render(template_dict)) - - # generate client confs - path = os.path.join(OUTPUT_DIR, CLIENT_OUTPUT_DIR) - os.makedirs(path, exist_ok=True) - with open(CLIENT_CONF_IN, "r") as f: - client_conf_temp = jinja2.Template(f.read()) - for i in range(len(conf.clients)): - template_dict['id'] = conf.clients[i].id - template_dict['port'] = conf.clients[i].port - template_dict['flow'] = conf.clients[i].flow - epath = os.path.join(path, conf.clients[i].name) - os.makedirs(epath, exist_ok=True) - with open(os.path.join(epath, SERVER_FN), "w") as f: - f.write(client_conf_temp.render(template_dict)) - - # chown - os.chown(OUTPUT_DIR, conf.uid, conf.gid) - for dirpath, dirnames, filenames in os.walk(OUTPUT_DIR): - os.chown(dirpath, conf.uid, conf.gid) - for fname in filenames: - os.chown(os.path.join(dirpath, fname), conf.uid, conf.gid) - print("Please find the generated files in the build directory. To start the stack, run docker-compose up -d in the build directory.") - -main() \ No newline at end of file diff --git a/server.in b/confs/concerto.quacker.net similarity index 57% rename from server.in rename to confs/concerto.quacker.net index 8df2fa8..ff5c96c 100644 --- a/server.in +++ b/confs/concerto.quacker.net @@ -1,8 +1,8 @@ { "log": { - "loglevel": "{{ loglevel }}", - "access": "/etc/xray/access.log", - "error": "/etc/xray/error.log" + "loglevel": "debug", + "access": "/opt/config/xray/access.log", + "error": "/opt/config/xray/error.log" }, "inbounds": [ { @@ -10,12 +10,15 @@ "protocol": "vless", "settings": { "clients": [ - {{ clients }} + { + "id": "zsy", + "flow": "xtls-rprx-direct" + } ], "decryption": "none", "fallbacks": [ { - "dest": "d2ray_nginx:80" + "dest": "localhost:80" } ] }, @@ -26,8 +29,8 @@ "alpn": ["http/1.1", "h2"], "certificates": [ { - "certificateFile": "/le-etc/letsencrypt/live/{{ subdomain }}.{{ domain }}/fullchain.pem", - "keyFile": "/le-etc/letsencrypt/live/{{ subdomain }}.{{ domain }}/privkey.pem" + "certificateFile": "/etc/letsencrypt/live/concerto.quacker.net/fullchain.pem", + "keyFile": "/etc/letsencrypt/live/concerto.quacker.net/privkey.pem" } ] } diff --git a/crontab b/crontab new file mode 100644 index 0000000..dd02b9a --- /dev/null +++ b/crontab @@ -0,0 +1 @@ +0 1 * * * certbot renew \ No newline at end of file diff --git a/docker-compose.in b/docker-compose.in deleted file mode 100644 index d6a7740..0000000 --- a/docker-compose.in +++ /dev/null @@ -1,60 +0,0 @@ -version: '3.0' - -networks: - br-d2ray: - external: false - driver: bridge - driver_opts: - com.docker.network.bridge.name: br-d2ray - enable_ipv6: false - ipam: - driver: default - config: - - subnet: 172.127.127.0/24 - -services: - d2ray_nginx: - container_name: d2ray_nginx - image: linuxserver/swag - restart: unless-stopped - cap_add: - - NET_ADMIN - networks: - - br-d2ray - environment: - - PUID={{ uid }} - - PGID={{ gid }} - - TZ=US/Eastern - - URL={{ domain }} - - SUBDOMAINS={{ subdomain }} - - VALIDATION=http - - EMAIL=dummy@dummy.com - - DHLEVEL=2048 - - ONLY_SUBDOMAINS={{ subdomain_only }} - - STAGING=false - ports: - - 80:80 - volumes: - - ./nginx:/config - healthcheck: - test: ["CMD", "curl", "-f", "http://{{ subdomain }}.{{ domain }}:80"] - interval: 5s - timeout: 3s - retries: 30 - - d2ray_xray: - container_name: d2ray_xray - image: teddysun/xray - restart: unless-stopped - depends_on: - d2ray_nginx: - condition: service_healthy - networks: - - br-d2ray - ports: - - 443:443 - volumes: - - ./nginx/etc:/le-etc - - ./xray:/etc/xray - -{{ watchtower }} diff --git a/gen_upload.sh b/gen_upload.sh new file mode 100644 index 0000000..e175605 --- /dev/null +++ b/gen_upload.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set +e + +mkdir -p ./uploads + +for filename in ./confs/*; do + fname=$(basename $filename) + fhash=$(echo -n '$fname' | openssl dgst -md5 | sed -E 's/\(stdin\)= (.*)/\1/') + openssl aes-256-cbc -md sha512 -pbkdf2 -in $filename -out ./uploads/$fhash.conf -k "sergeygorbunov" +done \ No newline at end of file diff --git a/nginx.in b/nginx.conf similarity index 58% rename from nginx.in rename to nginx.conf index b514a82..7477aa8 100644 --- a/nginx.in +++ b/nginx.conf @@ -1,17 +1,17 @@ geo $external { default 1; - 172.127.127.0/24 0; + localhost 0; } server { - listen 80 default_server; + listen 80 http http2 default_server; server_name {{subdomain}}.{{domain}}; if ($external) { return 301 https://$host$request_uri; } - root /config/www; - index index.html index.htm index.php; + root /var/lib/nginx/html; + index index.html; } diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..14c3cc1 --- /dev/null +++ b/run.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +set +e + +BUCKET_NAME="config.quacker.net" + +echo "===== Checking Environment Variables =====" +if [ -z "$FQDN" ]; then + echo "FQDN must be set" + exit 1 +else + echo "FQDN = $FQDN" +fi + +if [ -z "$SALT" ]; then + echo "SALT must be set" + exit 1 +else + echo "SALT = $SALT" +fi + +if [ -z "$KEY" ]; then + echo "KEY must be set" + exit 1 +else + echo "KEY = $KEY" +fi + +BUCKET_HASH=$(echo -n "$BUCKET_NAME" | openssl dgst -md5 | sed -E 's/\(stdin\)= (.*)/\1/') +echo "BUCKET_HASH= $BUCKET_HASH" + +echo "===== Setting Up Environment ======" +ln -s /opt/config/certs /etc/letsencrypt + +echo "===== Checking Certificates ====" +if [ ! -d "/etc/letsencrypt/live/$FQDN" ]; then + echo "Generating new certificates..." + certbot certonly -n --standalone -m dummy@dummy.com --agree-tos --no-eff-email -d "$FQDN" +else + echo "Certificate exists. Checking renewal..." + certbot renew +fi + +echo "===== Downloading configuration file =====" +hash=$(echo -n "$FQDN.$SALT" | openssl dgst -sha256 | sed -E 's/\(stdin\)= (.*)/\1/') +echo "Host hash is $hash" +wget http://$BUCKET_HASH.s3-website-us-west-1.amazonaws.com/config/$hash.conf -P /opt/ +openssl aes-256-cbc -d -md sha512 -pbkdf2 -in /opt/$hash.conf -out /opt/$FQDN.conf -k $KEY + +echo "===== Starting services =====" +crond -L /opt/config/logs/crond/log.txt +nginx -c /opt/nginx.conf + +echo "===== Starting xray =====" +/opt/xray/xray -c /opt/$FQDN.conf diff --git a/watchtower.in b/watchtower.in deleted file mode 100644 index a390258..0000000 --- a/watchtower.in +++ /dev/null @@ -1,8 +0,0 @@ - watchtower: - container_name: d2ray_watchtower - image: containrrr/watchtower - restart: unless-stopped - networks: - - br-d2ray - volumes: - - /var/run/docker.sock:/var/run/docker.sock