From 10078bbc39ea26b8f50acda0254a316bda4f9f16 Mon Sep 17 00:00:00 2001 From: gadget78 Date: Sat, 14 Jun 2025 00:12:00 +0100 Subject: [PATCH] this mainly adds port mapping and environment variables features for tenant use. - it also adds a fix and improves docker pulls, so there are no unneeded pulls, so not as likely to hit docker rate limits. - it also adds the ability to use a a proxy front end, so that domains can be linked into the ports assigned. which included a new evernode dns system. - it also adds a fix for local Lan exposure vulnerability. i will add more details on how the port mapping, vulnerability, and dns system works in an issue post for better readability. --- CMakeLists.txt | 4 +- dependencies/dns_evernode.sh | 200 +++++++++ dependencies/user-install.sh | 732 ++++++++++++++++++++++++++++++++- installer/sashimono-install.sh | 2 +- src/conf.cpp | 1 + 5 files changed, 919 insertions(+), 20 deletions(-) create mode 100644 dependencies/dns_evernode.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 9205a31..093899d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ add_dependencies(sagent ) add_custom_command(TARGET sagent POST_BUILD - COMMAND bash -c "cp -r ./dependencies/{hpfs,user-install.sh,user-uninstall.sh} ./build/" + COMMAND bash -c "cp -r ./dependencies/{hpfs,user-install.sh,dns_evernode.sh,user-uninstall.sh} ./build/" COMMAND tar xf ./dependencies/contract_template.tar -C ./build/ --no-same-owner COMMAND cp ./dependencies/hp.cfg ./build/contract_template/cfg/ COMMAND cp ./evernode-bootstrap-contract/src/bootstrap_upgrade.sh ./build/contract_template/contract_fs/seed/state/ @@ -79,7 +79,7 @@ target_precompile_headers(sagent PUBLIC src/pchheader.hpp) # Add target to generate the installer setup. add_custom_target(installer COMMAND mkdir -p ./build/installer - COMMAND bash -c "cp -r ./build/{sagent,sashi,hpfs,user-install.sh,user-uninstall.sh,contract_template} ./build/installer/" + COMMAND bash -c "cp -r ./build/{sagent,sashi,hpfs,user-install.sh,dns_evernode.sh,user-uninstall.sh,contract_template} ./build/installer/" COMMAND bash -c "cp -r ./installer/{docker-install.sh,docker-registry-install.sh,docker-registry-uninstall.sh,prereq.sh,sashimono-install.sh,sashimono-uninstall.sh} ./build/installer/" COMMAND bash -c "cp -r ./dependencies/{user-cgcreate.sh,libblake3.so} ./build/installer/" COMMAND bash -c "cp -r ./evernode-license.pdf ./build/installer/" diff --git a/dependencies/dns_evernode.sh b/dependencies/dns_evernode.sh new file mode 100644 index 0000000..ca8bee2 --- /dev/null +++ b/dependencies/dns_evernode.sh @@ -0,0 +1,200 @@ +#!/bin/sh + +# Load acme.sh helper functions +# . "${0%/*}/acme.sh" + +# Function to ADD a DNS TXT record +dns_evernode_add() { + full_domain="$1" # The full domain (e.g., _acme-challenge.example.com) + txtvalue="$2" # The TXT record value to add + apicall_success="false" # variable used to keep tabs if API call was a success + api_subdomain="https://dnsapi." # subdomain that API is setup on the evernode + nameserver_override="@8.8.8.8" # a nameserver to use for all the "digs", so as not to use the "host default one" + + _info "### Evernode DNS v.93 script running, this will collect all name servers that are held within $full_domain" + _info "### then it will poll each name server to add the TXT record, (substituting subdomain for ${api_subdomain}) as long as one works the script wil continue." + + # Step 1: Fetch authoritative nameservers for the parent domain (_acme-challenge.subdomain.main.com -> subdomain.main.com) + parent_domain="${full_domain#*.}" + # make sure we have zone correct + dots=$(echo "$parent_domain" | tr -cd '.' | wc -c) + if [ "$dots" -gt 1 ]; then + zone_parent_domain=$(dig +trace +time=1 +tries=1 "$parent_domain" $nameserver_override | + grep -v "^;" | + sed -n 's/^\([^[:space:]]\+\)[[:space:]]\+[0-9]\+[[:space:]]\+IN[[:space:]]\+NS.*/\1/p' | + sort | tail -n1) + + #### v1 sed | sed -n '/^[^;]/ {/NS[[:space:]]/s/^\([^[:space:]]\+\)[[:space:]]\+[0-9]\+[[:space:]]\+IN[[:space:]]\+NS.*/\1/p}' | tail -n 1) + ## used sed instead of, | awk '/^[^;]/ && $4 == "NS" {print $1}' + else + zone_parent_domain="$parent_domain" + fi + if [ -z "$zone_parent_domain" ]; then + _err "Unable to determine the authoritative zone for $full_domain." + return 1 + fi + _info "Authoritative zone for $full_domain is $zone_parent_domain." + + # now we 100% have zone, we can get all nameservers of root domain, ready for next step + ns_parent_domain=$(dig +short NS "$zone_parent_domain" "$nameserver_override") + if [ -z "$ns_parent_domain" ]; then + _err "No authoritative nameservers found for $parent_domain." + return 1 + else + _info "nameserver list secured, with $(echo "$ns_parent_domain" | wc -l) in list." + _debug "list of nameserver(s):\n$ns_parent_domain" + fi + + # Step 2: Iterate through the nameservers of the root domain. + for ns in $ns_parent_domain; do + _info "Querying $ns for NS records of $full_domain..." + + # Step 3: get full list of "NS records" held for _acme-challenge subdomain for domain we want SSL for. + ns_challenge_domain=$(dig +norecurse +noall +authority NS "$full_domain" @"$ns") + + # process the above NS records, store in new variable, so we can check validity (using sed to comply with hook plugin environment) + ns_records=$(echo "$ns_challenge_domain" | sed -n '/IN[[:space:]]\+NS/{s/.*[[:space:]]\([^[:space:]]\+\)$/\1/;p}') + + # Check if processed response is empty, if so, try next name server. + if [ -z "$ns_records" ]; then + _info "error in response from $ns. Trying next nameserver..." + _info "processed response \$ns_records:\n$ns_records" + _info "actual dig response from $ns:\n$ns_challenge_domain" + continue + else + _info "good response from $ns, with a list of $(echo "$ns_records" | wc -l) NS records to check..." + _debug "list of NS records(s):\n$ns_challenge_domain" + fi + + # Step 4: iterate through each NS record from the pre-prepared variable in Step 3 + for ns_record in $ns_records; do + _info "Processing NS record: $ns_record" + + # Extract the domain part (e.g., ns1.example.com → example.com) and add subdomain of API + ns_domain="${ns_record#*.}" + api_domain="${api_subdomain}${ns_domain}/addtxt" + _debug "using URL ${api_domain} for API command" + + # Step 5: Use _post() to send the API request + _post '{"domain": "'"${full_domain}"'", "txt": "'"${txtvalue}"'"}' "${api_domain}" "" "POST" "application/json" + _debug "response:${response}" + + # Check if the API call was successful + if _contains "$response" "successfully"; then + _info "Successfully updated $full_domain TXT record using API URL ${api_domain}" + apicall_success="true" + else + _err "Failed to update TXT record for domain $full_domain using API URL ${api_domain}" + fi + done + + if [ "$apicall_success" = "true" ]; then + _info "overall success in a setting a TXT record" + _info "###### script return" + return 0 + else + _err "overall error in setting any TXT records" + _info "###### script return" + return 1 + fi + done + _err "overall error in setting any TXT records" + _info "###### script return" + return 1 +} + +################################################################################################################################################ +# Function to REMOVE a DNS TXT record +dns_evernode_rm() { + full_domain="$1" # The full domain (e.g., _acme-challenge.example.com) + txtvalue="$2" # The TXT record value to add + apicall_success="false" # variable used to keep tabs if API call was a success + api_subdomain="https://dnsapi." + nameserver_override="@8.8.8.8" # a nameserver to use for all the "digs", so as not to use the "host default one" + + _info "### Evernode DNS v.9 script running, this will collect all name servers that are held within $full_domain" + _info "### then it will poll each name server to now remove the TXT record, (substituting subdomain for ${api_subdomain})." + + # Step 1: Fetch authoritative nameservers for the parent domain (_acme-challenge.subdomain.main.com -> subdomain.main.com) + parent_domain="${full_domain#*.}" + # make sure we have zone correct + dots=$(echo "$parent_domain" | tr -cd '.' | wc -c) + if [ "$dots" -gt 1 ]; then + zone_parent_domain=$(dig +trace +time=1 +tries=1 "$parent_domain" $nameserver_override | sed -n '/^[^;]/ {/NS[[:space:]]/s/^\([^[:space:]]\+\)[[:space:]]\+[0-9]\+[[:space:]]\+IN[[:space:]]\+NS.*/\1/p +}' | tail -n 1) + else + zone_parent_domain="$parent_domain" + fi + if [ -z "$zone_parent_domain" ]; then + _err "Unable to determine the authoritative zone for $full_domain." + return 1 + fi + _info "Authoritative zone for $full_domain is $zone_parent_domain." + + # now we 100% have zone, we can get all nameservers of root domain, ready for next step + ns_parent_domain=$(dig +short NS "$zone_parent_domain" "$nameserver_override") + if [ -z "$ns_parent_domain" ]; then + _err "No authoritative nameservers found for $parent_domain." + return 1 + else + _info "nameserver list secured, with $(echo "$ns_parent_domain" | wc -l) in list." + _debug "list of nameserver(s):\n$ns_parent_domain" + fi + + # Step 2: Iterate through the nameservers of the root domain. + for ns in $ns_parent_domain; do + _info "Querying $ns for NS records of $full_domain..." + + # Step 3: get full list of "NS records" held for _acme-challenge subdomain for domain we want SSL for. + ns_challenge_domain=$(dig +norecurse +noall +authority NS "$full_domain" @"$ns") + + # process the above NS records, store in new variable, so we can check validity (using sed to comply with hook plugin environment) + ns_records=$(echo "$ns_challenge_domain" | sed -n '/IN[[:space:]]\+NS/{s/.*[[:space:]]\([^[:space:]]\+\)$/\1/;p}') + + # Check if processed response is empty, if so, try next name server. + if [ -z "$ns_records" ]; then + _info "error in response from $ns. Trying next nameserver..." + _info "processed response \$ns_records:$ns_records" + _info "actual dig response from $ns:$ns_challenge_domain" + continue + else + _info "good response from $ns, with a list of $(echo "$ns_records" | wc -l) NS records to check..." + _debug "list of NS records(s):\n$ns_challenge_domain" + fi + + # Step 4: iterate through each NS record from the pre-prepared variable in Step 3 + for ns_record in $ns_records; do + _info "Processing NS record: $ns_record" + + # Extract the domain part (e.g., ns1.example.com → example.com) and add subdomain of API + ns_domain="${ns_record#*.}" + api_domain="${api_subdomain}${ns_domain}/rmtxt" + _debug "using URL ${api_domain} for API command" + + # Step 5: Use _post() to send the API request + _post '{"domain": "'"${full_domain}"'", "txt": "'"${txtvalue}"'"}' "${api_domain}" "" "POST" "application/json" + _debug "response:${response}" + + # Check if the API call was successful + if _contains "$response" "successfully"; then + _info "Successfully removed $full_domain TXT record using API URL ${api_domain}" + apicall_success="true" + else + _err "Failed to remove TXT record for domain $full_domain using API URL ${api_domain}" + fi + done + + if [ "$apicall_success" = "true" ]; then + _info "success in removing a TXT record" + _info "###### script return" + return 0 + else + _err "error in removing any TXT records" + _info "###### script return" + return 1 + fi + done + _err "overall error in setting any TXT records" + _info "###### script return" + return 1 +} \ No newline at end of file diff --git a/dependencies/user-install.sh b/dependencies/user-install.sh index 1285e8a..5fbc8d0 100755 --- a/dependencies/user-install.sh +++ b/dependencies/user-install.sh @@ -1,6 +1,7 @@ #!/bin/bash # Sashimono contract instance user installation script. # This is intended to be called by Sashimono agent. +version=0.99 # Check for user cpu and memory quotas. cpu=$1 @@ -37,10 +38,23 @@ script_dir=$(dirname "$(realpath "$0")") docker_bin=$script_dir/dockerbin docker_img_dir=$docker_bin/images docker_service="docker.service" -docker_pull_timeout_secs=120 +docker_pull_timeout_secs=180 cleanup_script=$user_dir/uninstall_cleanup.sh gp_udp_port_count=2 gp_tcp_port_count=2 +osversion=$(grep -ioP '^VERSION_ID=\K.+' /etc/os-release) + + +SA_CONFIG="/etc/sashimono/sa.cfg" +MBXRPL_CONFIG="/etc/sashimono/mb-xrpl/mb-xrpl.cfg" +TLS_TYPE=$(jq -r ".proxy.tls_type | select( . != null )" "$MBXRPL_CONFIG") +EVERNODE_HOSTNAME="$(jq -r ".hp.host_address | select( . != null )" "$SA_CONFIG")" + +# configured urls +DOCKER_AUTH_URL="https://auth.docker.io/token?service=registry.docker.io&scope=repository:" +DOCKER_REGISTRY_URL="https://registry-1.docker.io/v2/" +ACME_SH_URL="https://raw.githubusercontent.com/acmesh-official/acme.sh/master/acme.sh" +ACME_DNS_PLUGIN_URL="https://raw.githubusercontent.com/gadget78/sashimono/main/dependencies/dns_evernode.sh" # Check if users already exists. [ "$(id -u "$user" 2>/dev/null || echo -1)" -ge 0 ] && echo "HAS_USER,INST_ERR" && exit 1 @@ -82,10 +96,23 @@ function wait_for_dockerd() { } nofile_soft_limit=$(ulimit -n -S) +if [ $nofile_soft_limit -lt 250000 ]; then + ulimit -n 250000 + nofile_soft_limit=250000 +fi +max_instance_count=$(jq -r ".system.max_instance_count | select( . != null )" "$SA_CONFIG") +if [ -n "$max_instance_count" ] && [ "$max_instance_count" -gt 0 ]; then + nofile_soft_limit=$((nofile_soft_limit / ( max_instance_count + 1 ))) + echo "setting ulimit -n to $nofile_soft_limit" +else + echo "Error: max_instance_count is not valid or not found in $SA_CONFIG, defaulting ulimit to 55000" + nofile_soft_limit=55000 +fi nproc_soft_limit=$(ulimit -u -S) # Adding process and file descriptor limitations for the user before user creation echo "$user hard nofile $nofile_soft_limit" | tee -a /etc/security/limits.conf +echo "$user soft nofile $nofile_soft_limit" | tee -a /etc/security/limits.conf echo "$user hard nproc $nproc_soft_limit" | tee -a /etc/security/limits.conf # Setup user and dockerd service. @@ -129,11 +156,133 @@ echo "Adding disk quota to the group." setquota -g -F vfsv0 "$user" "$disk" "$disk" 0 0 / echo "Configured disk quota for the group." +# Extract additional port settings if present, 1st it splits everything after :, then replaces all -- with |, and uses that to create an array +echo +echo "# checking for any additional port config within image name :$docker_image" +IFS='|' read -r -a image_array <<< "$( echo $docker_image | cut -d':' -f2 | sed 's/--/|/g' )" +echo "captured additional docker settings, ${image_array[@]}" +docker_image_version="${image_array[0]}" +custom_docker_settings=false +custom_docker_domain="" +custom_docker_subdomain="" +custom_docker_domain_ssl="true" +internal_peer_port="$peer_port" +internal_user_port="$user_port" +internal_gptcp1_port="$gp_tcp_port_start" +internal_gpudp1_port="$gp_udp_port_start" +internal_gptcp2_port=$((gp_tcp_port_start + 1)) +internal_gpudp2_port=$((gp_udp_port_start + 1)) +internal_run_contract="" +internal_env1_key="KEY1" +internal_env1_value="1" +internal_env2_key="KEY2" +internal_env2_value="2" +internal_env3_key="KEY3" +internal_env3_value="3" +internal_env4_key="KEY4" +internal_env4_value="4" + +# Loop through the array to extract pairs +for ((i = 1; i < ${#image_array[@]}; i += 2)); do + image_array_name="${image_array[i]}" + image_array_value="${image_array[i + 1]}" + echo "found additional setting, name: \"$image_array_name\" value: \"$image_array_value\"" + if [[ -n "$image_array_name" ]]; then + if [[ "$image_array_name" == "domain" ]]; then + custom_docker_settings="true" + custom_docker_domain=$image_array_value + fi + if [[ "$image_array_name" == "subdomain" ]]; then + custom_docker_settings="true" + custom_docker_subdomain=$image_array_value + fi + if [[ "$image_array_name" == "ssl" ]]; then + custom_docker_settings="true" + if [[ "$image_array_value" == "false" ]];then custom_docker_domain_ssl="false"; fi + fi + if [[ "$image_array_name" == "peer" ]]; then + custom_docker_settings="true" + internal_peer_port=$image_array_value + fi + if [[ "$image_array_name" == "user" ]]; then + custom_docker_settings="true" + internal_user_port=$image_array_value + fi + if [[ "$image_array_name" == "gptcp1" ]]; then + custom_docker_settings="true" + internal_gptcp1_port=$image_array_value + fi + if [[ "$image_array_name" == "gpudp1" ]]; then + custom_docker_settings="true" + internal_gpudp1_port=$image_array_value + fi + if [[ "$image_array_name" == "gptcp2" ]]; then + custom_docker_settings="true" + internal_gptcp2_port=$image_array_value + fi + if [[ "$image_array_name" == "gpudp2" ]]; then + custom_docker_settings="true" + internal_gpudp2_port=$image_array_value + fi + if [[ "$image_array_name" == "contract" ]]; then + if [ "$image_array_value" == "true" ]; then + custom_docker_settings="true" + internal_run_contract="run /contract" + echo "found contract command, enabling \"run /contract\"" + fi + fi + if [[ "$image_array_name" == "env1" ]]; then + custom_docker_settings="true" + internal_env1_key=$(echo "$image_array_value" | cut -d'-' -f1) + internal_env1_value=${image_array_value#*-} + internal_env1_value=${internal_env1_value//__/ } + internal_env1_value=${internal_env1_value//../$} + echo "found env1, key>$internal_env1_key value>$internal_env1_value" + fi + if [[ "$image_array_name" == "env2" ]]; then + custom_docker_settings="true" + internal_env2_key=$(echo "$image_array_value" | cut -d'-' -f1) + internal_env2_value=${image_array_value#*-} + internal_env2_value=${internal_env2_value//__/ } + internal_env2_value=${internal_env2_value//../$} + echo "found env1, key>$internal_env2_key value>$internal_env2_value" + fi + if [[ "$image_array_name" == "env3" ]]; then + custom_docker_settings="true" + internal_env3_key=$(echo "$image_array_value" | cut -d'-' -f1) + internal_env3_value=${image_array_value#*-} + internal_env3_value=${internal_env3_value//__/ } + internal_env3_value=${internal_env3_value//../$} + echo "found env1, key>$internal_env3_key value>$internal_env3_value" + fi + if [[ "$image_array_name" == "env4" ]]; then + custom_docker_settings="true" + internal_env4_key=$(echo "$image_array_value" | cut -d'-' -f1) + internal_env4_value=${image_array_value#*-} + internal_env4_value=${internal_env4_value//__/ } + internal_env4_value=${internal_env4_value//../$} + echo "found env1, key>$internal_env4_key value>$internal_env4_value" + fi + fi +done + +# adjust docker_pull_image, and set a "default" custom_docker_image, only if custom settings have been detected +if [[ "$custom_docker_settings" == "true" ]]; then + docker_pull_image="$(echo "$docker_image" | cut -d':' -f1):${docker_image_version}" + echo "all additional port/custom settings found and saved, will be using a pull image of $docker_pull_image" + echo +else + docker_pull_image="$docker_image" + echo "no additional port/custom settings found in image tag" + echo +fi + # Setup env variables for the user. echo " export XDG_RUNTIME_DIR=$user_runtime_dir export PATH=$docker_bin:\$PATH -export DOCKER_HOST=$dockerd_socket" >>"$user_dir"/.bashrc +export DOCKER_HOST=$dockerd_socket +source /contract/env.vars" >>"$user_dir"/.bashrc echo "Updated user .bashrc." # Wait until user systemd is functioning. @@ -199,6 +348,23 @@ for ((i = 0; i < $gp_tcp_port_count; i++)); do fi done +# Creating AppArmor Profile for unpriviledged user on Ubuntu 24.04 +if [ "$osversion" == "24.04" ]; then + filename=$(echo /home/$user/bin/rootlesskit | sed -e s@^/@@ -e s@/@.@g) + cat < /etc/apparmor.d/$filename +abi , +include + +"/home/$user/bin/rootlesskit" flags=(unconfined) { + userns, + + include if exists +} +EOF + chown $user:$user /etc/apparmor.d/$filename + systemctl restart apparmor.service +fi + echo "Installing rootless dockerd for user." sudo -H -u "$user" PATH="$docker_bin":"$PATH" XDG_RUNTIME_DIR="$user_runtime_dir" "$docker_bin"/dockerd-rootless-setuptool.sh install @@ -208,7 +374,48 @@ docker_service_override_conf="$user_dir/.config/systemd/user/$docker_service.d/o sudo -H -u "$user" mkdir $user_dir/.config/systemd/user/$docker_service.d sudo -H -u "$user" touch $docker_service_override_conf echo "[Service] - Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" >$docker_service_override_conf +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns +" >"$docker_service_override_conf" + +# Create nftables (aka iptables), to block traffic to the local LAN subnet (for IPv4 and IPv6) +local_ip=$(hostname -I | awk '{print $1}' | xargs) +public_hostname_ip=$(dig +short "$EVERNODE_HOSTNAME" | head -n 1 | xargs) +if [[ "$local_ip" != "$public_hostname_ip" ]]; then + nft flush table ip docker_filter_$user_id 2>/dev/null + nft delete table ip docker_filter_$user_id 2>/dev/null + nft add table ip docker_filter_$user_id + nft add chain ip docker_filter_$user_id OUTPUT '{ type filter hook output priority 0 ; policy accept ; }' + + echo "detected local ip to $local_ip, allowing." + nft add rule ip docker_filter_$user_id OUTPUT meta skuid $user_id ip daddr $local_ip accept + + gateway=$(ip route show | grep default | awk '{print $3}') + if [ -z "$gateway" ]; then + echo "Warning: Could not detect gateway IP." + else + echo "detected gateway as $gateway, allowing." + nft add rule ip docker_filter_$user_id OUTPUT meta skuid $user_id ip daddr $gateway accept + fi + + proxy_ip=$(jq -r ".proxy.ip | select( . != null )" "$MBXRPL_CONFIG" ) + if [ -z "$proxy_ip" ]; then proxy_ip=$(jq -r ".proxy.npm_url | select( . != null )" "$MBXRPL_CONFIG" | awk -F[/:] '{print $4}'); fi + if [ -z "$proxy_ip" ]; then + echo "Warning: Could not detect proxy IP." + else + echo "detected proxy ip to $proxy_ip, allowing." + nft add rule ip docker_filter_$user_id OUTPUT meta skuid $user_id ip daddr $proxy_ip accept + fi + + lan_subnet=$(ip route | grep -oP '(\d+\.\d+\.\d+\.\d+/\d+)' | head -n 1) + if [ -z "$lan_subnet" ]; then + echo "Error: Could not detect LAN subnet." + else + echo "detected lan subnet ip to $lan_subnet, blocking/dropping" + nft add rule ip docker_filter_$user_id OUTPUT meta skuid $user_id ip daddr $lan_subnet drop + fi +else + echo "a stand alone evernode with no subnet detected, no extra ip table rules needed" +fi # We need to enable ipv6 configurations if outbound ipv6 address is specified. if [ "$outbound_ipv6" != "-" ] && [ "$outbound_net_interface" != "-" ]; then @@ -233,7 +440,15 @@ if [ "$outbound_ipv6" != "-" ] && [ "$outbound_net_interface" != "-" ]; then # Add the outbound ipv6 address to the specified network interface. ip addr add $outbound_ipv6 dev $outbound_net_interface - # Add instructions to the cleanup script so the outbound ip assignment will be removed upon user install. + # add rules to iptables, to restrict access to ipv6 subnet + if [[ "$local_ip" != "$public_hostname_ip" ]]; then + ipv6_gateway=$(ip -6 route show | grep default | awk '{print $3}') + nft add rule ip6 docker_filter_$user_id OUTPUT ip6 daddr "$ipv6_gateway" accept + ipv6_subnet=$(ip -6 route show | grep -v default | grep -oP '([0-9a-f:]+/\d+)' | head -n 1) + nft add rule ip6 docker_filter_$user_id OUTPUT ip6 daddr "$ipv6_subnet" drop + fi + + # Add instructions to the cleanup script so the outbound ip assignment will be removed upon user uninstall. echo "ip addr del $outbound_ipv6 dev $outbound_net_interface" >>$cleanup_script fi @@ -251,26 +466,65 @@ sudo -u "$user" XDG_RUNTIME_DIR="$user_runtime_dir" systemctl --user restart $do service_ready $docker_service || rollback "NO_DOCKERSVC" # Wait until docker daemon ready, If failed rollback. ! wait_for_dockerd && rollback "NO_DOCKERD" +echo "finished Installing rootless dockerd." -echo "Installed rootless dockerd." -# echo "Pulling the docker image $docker_image." -# DOCKER_HOST="$dockerd_socket" timeout --foreground -v -s SIGINT "$docker_pull_timeout_secs"s "$docker_bin"/docker pull "$docker_image" || rollback "DOCKER_PULL" -# echo "Docker image $docker_image pull complete." +mkdir -p $docker_img_dir +img_local_path=$docker_img_dir/$(echo "$docker_pull_image" | tr : -) +img_local_tar_path="$img_local_path.tar" -echo "Downloading the docker image $docker_image." -img_local_path=$docker_img_dir/$(echo "$docker_image" | tr : -) -"$docker_bin"/download-frozen-image-v2.sh $img_local_path $docker_image || rollback "DOCKER_PULL" +# Check if the image exists locally, and if it matches dockerhubs, also using $docker_pull_image for image name, due to any custom settings. +if [[ ! -d "$img_local_path" ]] || [[ ! -f "${img_local_path}/image_digest" ]]; then -img_local_tar_path=$img_local_path.tar -echo "Saving the downloaded image as a tarball: $img_local_tar_path" -tar -cvf $img_local_tar_path -C $img_local_path . || rollback "DOCKER_PULL" + #echo "Image $docker_image not found locally. Pulling from registry..." + #DOCKER_HOST="$dockerd_socket" timeout --foreground -v -s SIGINT "$docker_pull_timeout_secs"s "$docker_bin"/docker pull "$docker_image" || rollback "DOCKER_PULL" + #echo "image $docker_image pull complete." -echo "Loading the docker image $img_local_path." + echo "Image $docker_pull_image, or image hash not found locally, pulling docker image via frozen script..." + "$docker_bin"/download-frozen-image-v2.sh $img_local_path $docker_pull_image || rollback "DOCKER_PULL" + + echo "retrieving and saving image hash(digest) to file..." + TOKEN=$(curl -s "${DOCKER_AUTH_URL}$(echo "$docker_pull_image" | cut -d':' -f1):pull" | jq -r '.token') \ + && IMAGE_DIGEST=$(curl -s --head -H "Authorization: Bearer $TOKEN" ${DOCKER_REGISTRY_URL}$(echo "$docker_pull_image" | cut -d':' -f1)/manifests/${docker_image_version} | sed -n 's/.*[Dd]ocker-[Cc]ontent-[Dd]igest: \(sha256:[a-f0-9]*\).*/\1/p') + echo "$IMAGE_DIGEST" > ${img_local_path}/image_digest + + echo "Saving the downloaded image as a tarball: $img_local_tar_path" + tar -cvf $img_local_tar_path -C $img_local_path . || rollback "DOCKER_PULL" + echo "docker image saved as a tarball, $img_local_tar_path" +else + + echo "Image $docker_pull_image, already exists locally," + TOKEN=$(curl -s "{DOCKER_AUTH_URL}$(echo "$docker_pull_image" | cut -d':' -f1):pull" | jq -r '.token') \ + && IMAGE_DIGEST=$(curl -s --head -H "Authorization: Bearer $TOKEN" ${DOCKER_REGISTRY_URL}$(echo "$docker_pull_image" | cut -d':' -f1)/manifests/${docker_image_version} | sed -n 's/.*[Dd]ocker-[Cc]ontent-[Dd]igest: \(sha256:[a-f0-9]*\).*/\1/p') \ + && RATE_LIMIT_REMAINING=$(curl -s --head -s -H "Authorization: Bearer $TOKEN" ${DOCKER_REGISTRY_URL}$(echo "$docker_pull_image" | cut -d':' -f1)/manifests/${docker_image_version} | sed -n 's/.*[Rr]atelimit-remaining: \([0-9]*\).*/\1/p') + + # Check if re-pull is needed, and we have a good amount of "remaining" rate pulls left + if [[ "$IMAGE_DIGEST" != "$(cat "${img_local_path}/image_digest" 2>/dev/null)" ]] && [[ "$RATE_LIMIT_REMAINING" =~ ^[0-9]+$ ]] && [[ "$RATE_LIMIT_REMAINING" -gt 60 ]]; then + echo "local image hash not equal to docker hub image, and rate limit is above 60 (=${RATE_LIMIT_REMAINING}), re-pulling image, and saving as tarball..." + "$docker_bin"/download-frozen-image-v2.sh $img_local_path $docker_pull_image && tar -cvf $img_local_tar_path -C $img_local_path . || rollback "DOCKER_PULL" + echo "$IMAGE_DIGEST" > ${img_local_path}/image_digest + echo "docker image pulled, and saved as a tarball at $img_local_tar_path. and refreshed image digest record" + else + echo "File hash matches docker hub, AND the Rate limit result is above 60 (=${RATE_LIMIT_REMAINING})" + echo "local hash = $(cat ${img_local_path}/image_digest)" + echo "docker hash = ${IMAGE_DIGEST}" + echo "skipping image pull." + fi + +fi + +echo "Loading the docker image $img_local_tar_path." DOCKER_HOST="$dockerd_socket" "$docker_bin"/docker load -i "$img_local_tar_path" || rollback "DOCKER_PULL" -echo "Docker image $img_local_path pull complete." +echo "Docker image $img_local_tar_path load complete." -echo "Adding hpfs services for the instance." +echo "making sure pulled image has both the original "version" tag, and the tag with any custom settings" +DOCKER_HOST="$dockerd_socket" "$docker_bin"/docker tag ${docker_pull_image} ${docker_image} || rollback "DOCKER_PULL" +echo "Docker tag update complete, full image >$docker_image, original pulled image >$docker_pull_image" +echo + + + +echo "Adding hpfs, docker_recreate and depending on recreate docker_vars services for the instance." echo "[Unit] Description=Running and monitoring contract fs. @@ -298,7 +552,451 @@ RestartSec=5 [Install] WantedBy=default.target" >"$user_dir"/.config/systemd/user/ledger_fs.service + +## setup NPM+ if host has NPMplus installed +if [[ "$TLS_TYPE" == "NPMplus" ]]; then + echo "NPMplus install detected" + + if [[ -n "$custom_docker_domain" ]]; then + if [[ ! -f "/usr/bin/sashimono/acme.sh" ]]; then + echo "base acme.sh missing, re-installing" + wget -O /usr/bin/sashimono/acme.sh "$ACME_SH_URL" + chmod +x /usr/bin/sashimono/acme.sh + fi + if [[ ! -f "/usr/bin/sashimono/dns_evernode.sh" ]]; then + echo "base evernode dns plugin for acme.sh missing, re-installing" + wget -O /usr/bin/sashimono/dns_evernode.sh "$ACME_DNS_PLUGIN_URL" + chmod +x /usr/bin/sashimono/dns_evernode.sh + mkdir -p /root/.acme.sh + cp /usr/bin/sashimono/dns_evernode.sh /root/.acme.sh/dns_evernode.sh + fi + + mkdir -p $user_dir/.acme.sh/ + cp /usr/bin/sashimono/acme.sh $user_dir/.acme.sh/acme.sh + cp /usr/bin/sashimono/dns_evernode.sh $user_dir/.acme.sh/dns_evernode.sh + chown -R $user:$user $user_dir/.acme.sh/acme.sh + chown -R $user:$user $user_dir/.acme.sh/dns_evernode.sh + fi + +cat > "$user_dir"/.docker/domain_ssl_update.sh < \$(date)" && echo +version=$version +custom_docker_domain="$custom_docker_domain" +custom_docker_subdomain="$custom_docker_subdomain" +tls_type="$TLS_TYPE" +instance_slot=${user_port: -1} +web_port="$gp_tcp_port_start" +EVERNODE_HOSTNAME="$EVERNODE_HOSTNAME" + +# 1st check for blacklisted domain, and null if present. +if [[ -n "\$custom_docker_domain" ]]; then + blacklist_domains=\$(jq -r ".proxy.blacklist[] | select( . != null )" "$MBXRPL_CONFIG") + for blacklist_domain_check in \$blacklist_domains; do + if [[ "\$custom_docker_domain" == *"\$blacklist_domain_check"* ]]; then + echo "blacklisted domain used in domain request :\$blacklist_domain_check NOT adding domain!." + tls_type="failed" + break + fi + done +fi + +# 2nd check if domain has authorization to be used in this instance. +if [[ -n "\$custom_docker_domain" ]]; then + contract_publickey=\$(jq -r ".contract.bin_args | select( . != null )" "$user_dir/$contract_dir/cfg/hp.cfg") || true + domain_txtrecord=\$(dig +short +time=10 +tries=3 TXT "\$custom_docker_domain" @1.1.1.1) + if echo "\$domain_txtrecord" | grep -q "\$contract_publickey"; then + echo "contract pubic key, '\$contract_publickey'" + echo "domain authorized, as found in the TXT records of \$custom_docker_domain, :\$domain_txtrecord" + else + echo "contract pubic key, '\$contract_publickey'" + echo "domain NOT authorized as NOT found in the TXT records of \$custom_docker_domain, :\$domain_txtrecord" + echo "not adding domain !" + tls_type="failed" + fi +fi + +# setup domain on NPM+ (if requested. passed checks above, and host has NPMplus installed) +if [[ "\$tls_type" == "NPMplus" ]]; then + echo "custom domain initial checks passed, continuing..." + echo + + NPM_URL="$(jq -r ".proxy.npm_url | select( . != null )" "$MBXRPL_CONFIG")" + NPM_TOKEN="$(jq -r ".npm.token | select( . != null )" "$(jq -r ".proxy.npm_tokenPath | select( . != null )" "$MBXRPL_CONFIG")")" + NPM_CERT_ID_WILD="false" + NPM_CERT_UPDATE="false" + + + if [[ -z "\$NPM_URL" || -z "\$NPM_TOKEN" ]]; then + echo "NPMplus URL, or Token not set... url=\"\$NPM_URL\" token=\"\$NPM_TOKEN\"" + else + + if [[ -n "\$custom_docker_subdomain" ]]; then + custom_docker_domain="\$custom_docker_subdomain.\${EVERNODE_HOSTNAME#*.}" + echo "subdomain request detected, assigning domain as \$custom_docker_domain" + fi + if [[ -z "\$custom_docker_domain" ]]; then + custom_docker_domain="\${EVERNODE_HOSTNAME%%.*}-\${instance_slot}.\${EVERNODE_HOSTNAME#*.}" + custom_docker_subdomain="\${custom_docker_domain%%.*}" + echo "no custom domain settings found, setting up a default route of, \$custom_docker_domain" + fi + + # get SSL files if needed + if [[ "$custom_docker_domain_ssl" == "true" ]]; then + echo "SSL request detected... " + NPM_CERT_EMAIL=\$(jq -r ".host.emailAddress | select( . != null )" "$MBXRPL_CONFIG") + NPM_CERT_LIST=\$(curl -k -s -m 100 -X GET -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/certificates || echo "" ) + NPM_CERT_ID=\$(echo "\$NPM_CERT_LIST" | jq -r '[.[] | select(.nice_name? == "'"\$custom_docker_domain"'") | .id // empty] | if length == 0 then "" else .[] end' || echo "" ) + # check for wildcard domain too (mainly needed for subdomain/defaulted domain) + if [ "\$NPM_CERT_ID" == "" ]; then + NPM_CERT_ID=\$(echo "\$NPM_CERT_LIST" | jq -r '[.[] | select(.nice_name? == "*.'"\${custom_docker_domain#*.}"'") | .id // empty] | if length == 0 then "" else .[] end' || echo "" ) + if [ -n "\$NPM_CERT_ID" ]; then + echo "wildcard SSL file detected" + NPM_CERT_ID_WILD="true" + NPM_CERT_PROVIDER="\$(echo "\$NPM_CERT_LIST" | jq -r '[.[] | select(.nice_name? == "*.'"\${custom_docker_domain#*.}"'") | .provider // empty] | if length == 0 then "" else .[] end' || echo "" )" + NPM_CERT_EXPIRE="\$(echo "\$NPM_CERT_LIST" | jq -r '[.[] | select(.nice_name? == "*.'"\${custom_docker_domain#*.}"'") | .expires_on // empty] | if length == 0 then "" else .[] end' || echo "" )" + NPM_CERT_EXPIRE_MONTH="\$(echo "\$NPM_CERT_EXPIRE" | cut -d'-' -f2)" + if [ "\$NPM_CERT_PROVIDER" == "other" ]; then + if [ "\$NPM_CERT_EXPIRE_MONTH" -le "\$(date +%m)" ]; then + NPM_CERT_UPDATE="true" + fi + fi + fi + else + NPM_CERT_ID_WILD="false" + NPM_CERT_PROVIDER="\$(echo "\$NPM_CERT_LIST" | jq -r '[.[] | select(.nice_name? == "'"\${custom_docker_domain}"'") | .provider // empty] | if length == 0 then "" else .[] end' || echo "" )" + NPM_CERT_EXPIRE="\$(echo "\$NPM_CERT_LIST" | jq -r '[.[] | select(.nice_name? == "'"\${custom_docker_domain}"'") | .expires_on // empty] | if length == 0 then "" else .[] end' || echo "" )" + NPM_CERT_EXPIRE_MONTH="\$(echo "\$NPM_CERT_EXPIRE" | cut -d'-' -f2)" + if [ "\$NPM_CERT_PROVIDER" == "other" ]; then + if [ "\$NPM_CERT_EXPIRE_MONTH" -le "\$(date +%m)" ]; then + NPM_CERT_UPDATE="true" + fi + fi + fi + #echo "ID check point NPM_CERT_ID:>\$NPM_CERT_ID<" + + if [[ "\$NPM_CERT_ID" == "" || "\$NPM_CERT_UPDATE" == "true" ]]; then + echo "files for domain \$custom_docker_domain now being created (updating=\${NPM_CERT_UPDATE} provider=\${NPM_CERT_PROVIDER} expiry=\${NPM_CERT_EXPIRE} expiry month=\${NPM_CERT_EXPIRE_MONTH})... " + if [ -z "\$custom_docker_subdomain" ]; then + echo "true custom tenant domain detected, using acme.sh DNS-01 and evernodes DNS API to create SSL files, and upload to NPM+..." + mkdir -p $user_dir/$contract_dir/tls + $user_dir/.acme.sh/acme.sh --issue \\ + --server letsencrypt \\ + --dns dns_evernode \\ + --domain "\$custom_docker_domain" --force \\ + --accountemail "\$NPM_CERT_EMAIL" \\ + --nocron \\ + --noprofile \\ + --useragent "evernode-domain-system" \\ + --cert-file $user_dir/$contract_dir/tls/cert.pem \\ + --key-file $user_dir/$contract_dir/tls/privkey.pem \\ + --ca-file $user_dir/$contract_dir/tls/chain.pem \\ + --fullchain-file $user_dir/$contract_dir/tls/fullchain.pem && + chown -R $user:$user $user_dir/$contract_dir/tls && + if [ "\$NPM_CERT_UPDATE" == "false" ]; then + NPM_CERT_ADD=\$(curl -k -sS -m 100 -X POST -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" -d '{"provider":"other","nice_name":"'"\$custom_docker_domain"'","domain_names":["'"\$custom_docker_domain"'"],"meta":{ }}' \$NPM_URL/api/nginx/certificates ) && + NPM_CERT_ID=\$(jq -r '.id' <<< "\$NPM_CERT_ADD") && + echo "created new certificate, and entry for host domain \$custom_docker_domain, ID is \$NPM_CERT_ID, now uploading to NPM+..." + else echo "created new certificate, for host domain \$custom_docker_domain, updating existing NPM+ ID \$NPM_CERT_ID, now uploading to..." + fi && \\ + NPM_CERT_UPLOAD=\$(curl -k -X POST "\$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID/upload" -H "Authorization: Bearer \$NPM_TOKEN" -F "certificate=@$user_dir/$contract_dir/tls/cert.pem" -F "certificate_key=@$user_dir/$contract_dir/tls/privkey.pem") \\ + || { \\ + echo "failed to create, add certificate ID, or Upload file, this WILL cause issues. ERROR; debug_ADD:\$NPM_CERT_ADD debug_ID:\$NPM_CERT_ID debug_UPLOAD:\$NPM_CERT_UPLOAD"; \\ + echo "flushing any entries from NPMplus for domain \$custom_docker_domain..."; \\ + curl -k -m 100 -X DELETE -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID; \\ + NPM_CERT_ID=""; + } + fi + if [[ -z "\$NPM_CERT_ID" || "\$NPM_CERT_ID" == "null" ]]; then + if [ -z "\$custom_docker_subdomain" ]; then echo "certificate creation via DNS-01 method failed or was skipped."; fi + echo + echo "trying via NPM+ with a more standard HTTP-01 method..." + NPM_CERT_ADD=\$(curl -k -s -m 100 -X POST -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" -d '{"provider":"letsencrypt","nice_name":"'"\$custom_docker_domain"'","domain_names":["'"\$custom_docker_domain"'"],"meta":{"letsencrypt_email":"'"\$NPM_CERT_EMAIL"'","letsencrypt_agree":true,"dns_challenge":false}}' \$NPM_URL/api/nginx/certificates ) || echo "failed to create certificate for this evernode, this WILL cause issues. ERROR; debug: \$NPM_CERT_ADD" + NPM_CERT_ID=\$(jq -r '.id' <<< "\$NPM_CERT_ADD") && echo "created new certificate for host domain \$custom_docker_domain, ID is \$NPM_CERT_ID" || echo "failed to find certificate ID, this WILL cause issues. ERROR; debug1: \$NPM_CERT_ADD debug2: \$NPM_CERT_ID" + if [[ -z "\$NPM_CERT_ID" || "\$NPM_CERT_ID" == "null" ]]; then + echo "certificate add failed via http-01 method, setting up domain with no SSL. debug_ADD:\$NPM_CERT_ADD debug_ID:\$NPM_CERT_ID" + echo "flushing any broken ssl entries in NPMplus for domain \$custom_docker_domain" + NPM_CERT_LIST=\$(curl -k -s -m 100 -X GET -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/certificates || echo "" ) + NPM_CERT_ID=\$(echo "\$NPM_CERT_LIST" | jq -r '[.[] | select(.nice_name? == "'"\$custom_docker_domain"'") | .id // empty] | if length == 0 then "" else .[] end' || echo "" ) + NPM_CERT_DELETE=\$(curl -k -m 100 -X DELETE -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID) + echo "flushed? debug_ID:\$NPM_CERT_ID debug_DELETE:\$NPM_CERT_DELETE" + NPM_CERT_ID_STRING=",\"certificate_id\":0,\"ssl_forced\":0" + else + echo "certificate added, for host domain \$custom_docker_domain, with ID:\$NPM_CERT_ID WILDCARD:\$NPM_CERT_ID_WILD" + mkdir -p $user_dir/$contract_dir/tls + if { + curl -k --output $user_dir/$contract_dir/tls/tls_files.zip -m 10 -X GET -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID/download && + unzip $user_dir/$contract_dir/tls/tls_files.zip -d $user_dir/$contract_dir/tls && + echo "downloaded certificate ID \$NPM_CERT_ID for evernode" + }; then + for tls_file in $user_dir/$contract_dir/tls/*.pem; do + tls_newname=\$(echo \$(basename \$tls_file) | sed 's/[0-9]*//g') + mv "\$tls_file" "$user_dir/$contract_dir/tls/\$tls_newname" + done + chown -R $user:$user $user_dir/$contract_dir/tls + if [[ "\$NPM_CERT_ID_WILD" == "false" ]]; then echo "curl -k -m 100 -X DELETE -H \"Content-Type: application/json; charset=UTF-8\" -H \"Authorization: Bearer \$NPM_TOKEN\" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID" >>$cleanup_script; fi + NPM_CERT_ID_STRING=",\"certificate_id\":\$NPM_CERT_ID,\"ssl_forced\":1" + else + echo "failed to download and unzip certificate ID \$NPM_CERT_ID, setting up domain with no SSL." + echo "flushing broken ssl certificate with ID \$NPM_CERT_ID from NPMplus." + curl -k -m 100 -X DELETE -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID + NPM_CERT_ID_STRING=",\"certificate_id\":0,\"ssl_forced\":0" + fi + fi + else + echo "certificate added, for host domain \$custom_docker_domain, with ID:\$NPM_CERT_ID WILDCARD:\$NPM_CERT_ID_WILD" + if [[ "\$NPM_CERT_ID_WILD" == "false" ]]; then echo "curl -k -m 100 -X DELETE -H \"Content-Type: application/json; charset=UTF-8\" -H \"Authorization: Bearer \$NPM_TOKEN\" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID" >>$cleanup_script; fi + NPM_CERT_ID_STRING=",\"certificate_id\":\$NPM_CERT_ID,\"ssl_forced\":1" + fi + elif [ "\$NPM_CERT_PROVIDER" != "other" ]; then + echo "found existing certificate for host domain \$custom_docker_domain, with ID:\$NPM_CERT_ID WILDCARD:\$NPM_CERT_ID_WILD" + mkdir -p $user_dir/$contract_dir/tls + if { + curl -k --output $user_dir/$contract_dir/tls/tls_files.zip -m 10 -X GET -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID/download && + unzip $user_dir/$contract_dir/tls/tls_files.zip -d $user_dir/$contract_dir/tls && + echo "downloaded certificate ID \$NPM_CERT_ID for evernode" + }; then + for tls_file in $user_dir/$contract_dir/tls/*.pem; do + tls_newname=\$(echo \$(basename \$tls_file) | sed 's/[0-9]*//g') + mv "\$tls_file" "$user_dir/$contract_dir/tls/\$tls_newname" + done + chown -R $user:$user $user_dir/$contract_dir/tls + if [[ "\$NPM_CERT_ID_WILD" == "false" ]]; then echo "curl -k -m 100 -X DELETE -H \"Content-Type: application/json; charset=UTF-8\" -H \"Authorization: Bearer \$NPM_TOKEN\" \$NPM_URL/api/nginx/certificates/\$NPM_CERT_ID" >>$cleanup_script; fi + NPM_CERT_ID_STRING=",\"certificate_id\":\$NPM_CERT_ID,\"ssl_forced\":1" + else + echo "failed to download and unzip certificate ID \$NPM_CERT_ID, setting up domain with no SSL." + NPM_CERT_ID_STRING=",\"certificate_id\":0,\"ssl_forced\":0" + fi + elif [ "\$NPM_CERT_PROVIDER" == "other" ]; then + echo "found custom certificate for host domain \$custom_docker_domain, its already in date, with ID=\$NPM_CERT_ID, expiry=\$NPM_CERT_EXPIRE expiry month=\$NPM_CERT_EXPIRE_MONTH, WILDCARD=\$NPM_CERT_ID_WILD" + NPM_CERT_ID_STRING=",\"certificate_id\":\$NPM_CERT_ID,\"ssl_forced\":1" + fi + else + echo "SSL not requested. " + NPM_CERT_ID_STRING=",\"certificate_id\":0,\"ssl_forced\":0" + fi + echo + + # ADD the domain to proxy_host list + NPM_PROXYHOSTS_LIST=\$( { curl -k -s -m 100 -X GET -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" \$NPM_URL/api/nginx/proxy-hosts || { msg_error "something went wrong getting NPM list of proxy hosts"; NPM_PROXYHOSTS_LIST={}; }; } ) + NPM_PROXYHOSTS_ID=\$( { echo "\$NPM_PROXYHOSTS_LIST" | jq -r '.[] | select(.domain_names[] == "'"\$custom_docker_domain"'") | .id' || echo ""; } ) + if [ "\$NPM_PROXYHOSTS_ID" == "" ]; then + echo "adding new proxy host domain \$custom_docker_domain using NPM_CERT_ID_STRING: \$NPM_CERT_ID_STRING" + NPM_ADD_RESPONSE=\$( { curl -k -s -m 100 -X POST -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" -d '{"domain_names":["'"\${custom_docker_domain//www./}"'","www.'"\${custom_docker_domain//www./}"'"],"forward_host":"'"\$(hostname -I | xargs | cut -d' ' -f1)"'","forward_port":'"\$web_port"',"access_list_id":0'"\$NPM_CERT_ID_STRING"',"caching_enabled":0,"block_exploits":1,"advanced_config":"add_header X-Served-By '"\$EVERNODE_HOSTNAME"';","meta":{"letsencrypt_agree":true,"nginx_online":true},"allow_websocket_upgrade":1,"http2_support":0,"forward_scheme":"https","locations":[],"hsts_enabled":0,"hsts_subdomains":0}' \$NPM_URL/api/nginx/proxy-hosts || { echo "something went wrong when adding \$custom_docker_domain proxy host"; NPM_ADD_RESPONSE="error"; }; } ) + NPM_ADD_RESPONSE_CHECK=\$(jq -r '.enabled // "no enabled entry"' <<< "\$NPM_ADD_RESPONSE" || echo "jq error, no json output?") + if [[ "\$NPM_ADD_RESPONSE_CHECK" == "1" || "\$NPM_ADD_RESPONSE_CHECK" == "true" ]]; then + echo "added new proxy host to NPM with domain \$custom_docker_domain" + NPM_PROXYHOSTS_ID=\$( echo "\$NPM_ADD_RESPONSE" | jq -r '.id') + echo "curl -k -s -m 100 -X DELETE -H \"Content-Type: application/json; charset=UTF-8\" -H \"Authorization: Bearer \$NPM_TOKEN\" \$NPM_URL/api/nginx/proxy-hosts/\$NPM_PROXYHOSTS_ID >/dev/null 2>&1" >>$cleanup_script + else + echo "failed to add new proxy host domain on NPM+ \$custom_docker_domain, this will cause issues connecting via this domain. (debug_check:\$NPM_ADD_RESPONSE_CHECK debug_response:\$NPM_ADD_RESPONSE )" + fi + elif [[ -z "\$custom_docker_subdomain" ]]; then + echo "proxy host already on NPM domain \$custom_docker_domain, updating (using a NPM_CERT_ID_STRING: \$NPM_CERT_ID_STRING)..." + NPM_EDIT_RESPONSE=\$( { curl -k -s -m 100 -X PUT -H "Content-Type: application/json; charset=UTF-8" -H "Authorization: Bearer \$NPM_TOKEN" -d '{"domain_names":["'"\${custom_docker_domain//www./}"'","www.'"\${custom_docker_domain//www./}"'"],"forward_host":"'"\$(hostname -I | xargs | cut -d' ' -f1)"'","forward_port":'"\$web_port"',"access_list_id":0'"\$NPM_CERT_ID_STRING"',"caching_enabled":0,"block_exploits":1,"advanced_config":"add_header X-Served-By '"\$EVERNODE_HOSTNAME"';","meta":{"letsencrypt_agree":true,"nginx_online":true},"allow_websocket_upgrade":1,"http2_support":0,"forward_scheme":"https","locations":[],"hsts_enabled":0,"hsts_subdomains":0}' \$NPM_URL/api/nginx/proxy-hosts/\$NPM_PROXYHOSTS_ID || { echo "something went wrong when updating \$custom_docker_domain proxy host"; NPM_EDIT_RESPONSE="error"; }; }) + NPM_EDIT_RESPONSE_CHECK=\$(jq -r '.enabled // "no enabled entry"' <<< "\$NPM_EDIT_RESPONSE" || echo "jq error, no json output?") + if [[ "\$NPM_EDIT_RESPONSE_CHECK" == "1" || "\$NPM_EDIT_RESPONSE_CHECK" == "true" ]]; then + echo "updated proxy host with domain \$custom_docker_domain" + echo "curl -k -s -m 100 -X DELETE -H \"Content-Type: application/json; charset=UTF-8\" -H \"Authorization: Bearer \$NPM_TOKEN\" \$NPM_URL/api/nginx/proxy-hosts/\$NPM_PROXYHOSTS_ID >/dev/null 2>&1" >>$cleanup_script + else + echo "failed to edit proxy host domain on NPM+, this will cause issues connecting via this domain. ( debug_check:\$NPM_EDIT_RESPONSE_CHECK debug_response:\$NPM_EDIT_RESPONSE )" + fi + fi + fi +elif [[ "\$tls_type" == "letsencrypt" ]]; then + echo "todo: add support for stand alone evernodes via direct nginx" + # this can be utilized by adding nginx directly on the host, and then using the etc/nginx/site-enabled or sites-available, as well as changing how cert bot gets utilized. but its VERY possible. + echo +elif [[ "\$tls_type" == "" ]]; then + echo "no supporting proxy settings to handle domain management config." + echo +elif [[ "\$tls_type" == "failed" ]]; then + echo "custom domain checks failed." + echo +else + echo "no custom domain setup triggered... " + echo +fi +cp $user_dir/.docker/domain_ssl_update.log $user_dir/$contract_dir/tls/domain_ssl_update.log + +echo "............." +EOF +chmod +x $user_dir/.docker/domain_ssl_update.sh +chown $user:$user $user_dir/.docker/domain_ssl_update.sh +fi + + + +if [[ -n "$custom_docker_subdomain" ]]; then + custom_docker_domain="$custom_docker_subdomain.${EVERNODE_HOSTNAME#*.}" + echo "subdomain request detected, assigning domain as $custom_docker_domain" +fi +if [[ -z "$custom_docker_domain" ]]; then + custom_docker_domain="${EVERNODE_HOSTNAME%%.*}-${instance_slot}.${EVERNODE_HOSTNAME#*.}" + custom_docker_subdomain="${custom_docker_domain%%.*}" + echo "no custom domain settings found, setting up a default route of, $custom_docker_domain" +fi + +cat > $user_dir/.docker/env.vars < "$user_dir"/.docker/docker_recreate.sh <\$CONTAINER_NAME"; exit 1;fi + +TIMEOUT_SECONDS=180 +INSPECT_DATA=\$(${docker_bin}/docker inspect \$CONTAINER_NAME) +PORTS=\$(echo \$INSPECT_DATA | jq -r '.[0].HostConfig.PortBindings') +MOUNT_SOURCE=\$(echo \$INSPECT_DATA | jq -r '.[0].HostConfig.Mounts[0].Source') +MOUNT_TARGET=\$(echo \$INSPECT_DATA | jq -r '.[0].HostConfig.Mounts[0].Target') +IMAGE=\$(echo \$INSPECT_DATA | jq -r '.[0].Config.Image') + +# Set Docker configs +export DOCKER_HOST="unix://${user_runtime_dir}/docker.sock" + +# Build the docker create command +docker_create_command="timeout --foreground -v -s SIGINT \${TIMEOUT_SECONDS}s ${docker_bin}/docker create -t -i --stop-signal=SIGINT --log-driver local --log-opt max-size=5m --log-opt max-file=2 --name=\${CONTAINER_NAME}" + +# Set the ports +for port in \$(echo \$PORTS | jq -r 'to_entries | .[] | .key'); do + external_port=\$(echo \$PORTS | jq -r ".\\"\$port\\"[0].HostPort") + internal_port=\$port + if [[ "${peer_port}" == "\$external_port" ]]; then internal_port="${internal_peer_port}/\${port#*/}"; fi + if [[ "${user_port}" == "\$external_port" ]]; then internal_port="${internal_user_port}/\${port#*/}"; fi + if [[ "${gp_tcp_port_start}" == "\$external_port" ]]; then internal_port="${internal_gptcp1_port}/\${port#*/}"; fi + if [[ "${gp_udp_port_start}" == "\$external_port" ]]; then internal_port="${internal_gpudp1_port}/\${port#*/}"; fi + if [[ "$((gp_tcp_port_start + 1))" == "\$external_port" ]]; then internal_port="${internal_gptcp2_port}/\${port#*/}"; fi + if [[ "$((gp_udp_port_start + 1))" == "\$external_port" ]]; then internal_port="${internal_gpudp2_port}/\${port#*/}"; fi + echo "EXTERNAL \$external_port INTERNAL \$internal_port" + docker_create_command="\$docker_create_command -p \$external_port:\$internal_port" +done + +# Add Environment variables +docker_create_command="\$docker_create_command --env-file $user_dir/.docker/env.vars" + +# Add security options +docker_create_command="\$docker_create_command --security-opt seccomp=unconfined --security-opt apparmor=unconfined" + +# Add restart policy +docker_create_command="\$docker_create_command --restart unless-stopped" + +# Add volume binding +docker_create_command="\$docker_create_command --mount type=bind,source=\${MOUNT_SOURCE},target=\${MOUNT_TARGET}" + +# handle original container, and service +${docker_bin}/docker stop \$CONTAINER_NAME +${docker_bin}/docker rm \$CONTAINER_NAME + +# Execute the docker create command +echo "docker_create_command built lets run >\$docker_create_command " +\$docker_create_command \${IMAGE} $internal_run_contract +${docker_bin}/docker start \$CONTAINER_NAME +EOF +chmod +x "$user_dir"/.docker/docker_recreate.sh + + +# set up a service to run the docker recreate.sh AFTER docker has created the original container +echo "[Unit] +Description=Docker Create Event Watcher +After=docker.service +Requires=docker.service + +[Service] +Environment="USER_DIR=/home/username" +ExecStart=/bin/bash -c ' \\ + ${docker_bin}/docker events --filter event=create | \\ + while read -r create_event; do \\ + echo \"Handling event: ${create_event}\" >> \"${user_dir}/.docker/docker_recreate.log\"; \\ + bash \"${user_dir}/.docker/domain_ssl_update.sh\" 2>&1 | tee -a ${user_dir}/.docker/domain_ssl_update.log; \\ + cp $user_dir/.docker/env.vars $user_dir/$contract_dir/env.vars; \\ + echo \"0 0 */7 * * sleep \$((RANDOM*3540/32768)) && /usr/bin/bash ${user_dir}/.docker/domain_ssl_update.sh 2>&1 | tee -a ${user_dir}/.docker/domain_ssl_update.log\" | crontab -; \\ + bash \"${user_dir}/.docker/docker_recreate.sh\" 2>&1 | tee -a ${user_dir}/.docker/docker_recreate.log; \\ + break;\\ + done' +Restart=on-failure +Type=simple +SuccessExitStatus=0 143 + +[Install] +WantedBy=default.target" > "$user_dir"/.config/systemd/user/docker_recreate.service + sudo -u "$user" XDG_RUNTIME_DIR="$user_runtime_dir" systemctl --user daemon-reload +sudo -u "$user" XDG_RUNTIME_DIR="$user_runtime_dir" systemctl --user enable docker_recreate.service +sudo -u "$user" XDG_RUNTIME_DIR="$user_runtime_dir" systemctl --user start docker_recreate.service +echo "sudo -u \"$user\" XDG_RUNTIME_DIR=\"$user_runtime_dir\" systemctl --user stop docker_recreate.service" >>$cleanup_script +echo "sudo -u \"$user\" XDG_RUNTIME_DIR=\"$user_runtime_dir\" systemctl --user disable docker_recreate.service" >>$cleanup_script +echo "sudo nft flush table ip docker_filter_$user_id 2>/dev/null && sudo nft delete table ip docker_filter_$user_id 2>/dev/null && echo \"Cleaned up docker_filter_$user_id table\"" >>$cleanup_script +echo "cat $user_dir/.docker/domain_ssl_update.log >> /root/domain_ssl_update.log" >>$cleanup_script +chown -R $user:$user $cleanup_script + + +else + + echo "no custom port or other user settings found. setting up recreate service to copy .vars file and default domain-farwading/proxy-host" + if [[ "$TLS_TYPE" == "NPMplus" ]]; then + domain_ssl_update_1='bash "'${user_dir}'/.docker/domain_ssl_update.sh" 2>&1 | tee -a '${user_dir}'/.docker/domain_ssl_update.log' + domain_ssl_update_2='echo "0 0 */7 * * sleep $((RANDOM*3540/32768)) && /usr/bin/bash '${user_dir}'/.docker/domain_ssl_update.sh 2>&1 | tee -a '${user_dir}'/.docker/domain_ssl_update.log" | crontab -' + else + domain_ssl_update_1='echo "no NPMplus intalled on host,"' + domain_ssl_update_2='not setting up any domain farwarding."' + fi +# set up a service to copy in the .vars file AFTER docker has created the original container. (as container/image needs to be created, before we can copy it in) +echo "[Unit] +Description=Docker env.vars file copier, proxy and firewall setup. +After=docker.service +Requires=docker.service + +[Service] +Environment="USER_DIR=/home/username" +ExecStart=/bin/bash -c ' \\ + ${docker_bin}/docker events --filter event=create | \\ + while read -r create_event; do \\ + cp $user_dir/.docker/env.vars $user_dir/$contract_dir/env.vars; \\ + $domain_ssl_update_1; \\ + $domain_ssl_update_2; \\ + break; \\ + done' +Restart=false +Type=simple +SuccessExitStatus=0 143 + +[Install] +WantedBy=default.target" > "$user_dir"/.config/systemd/user/docker_vars.service + +sudo -u "$user" XDG_RUNTIME_DIR="$user_runtime_dir" systemctl --user daemon-reload +sudo -u "$user" XDG_RUNTIME_DIR="$user_runtime_dir" systemctl --user enable docker_vars.service +sudo -u "$user" XDG_RUNTIME_DIR="$user_runtime_dir" systemctl --user start docker_vars.service +echo "sudo -u \"$user\" XDG_RUNTIME_DIR=\"$user_runtime_dir\" systemctl --user stop docker_vars.service" >>$cleanup_script +echo "sudo -u \"$user\" XDG_RUNTIME_DIR=\"$user_runtime_dir\" systemctl --user disable docker_vars.service" >>$cleanup_script +echo "sudo nft flush table ip docker_filter_$user_id 2>/dev/null && sudo nft delete table ip docker_filter_$user_id 2>/dev/null && echo \"Cleaned up docker_filter_$user_id table\"" >>$cleanup_script +echo "cat $user_dir/.docker/domain_ssl_update.log >> /root/domain_ssl_update.log" >>$cleanup_script +chown -R $user:$user $cleanup_script + +fi # In the Sashimono configuration, CPU time is 1000000us Sashimono is given max_cpu_us out of it. # Instance allocation is multiplied by number of cores to determined the number of cores per instance and devided by 10 since cfs_period_us is set to 100000us diff --git a/installer/sashimono-install.sh b/installer/sashimono-install.sh index a851262..cb87383 100755 --- a/installer/sashimono-install.sh +++ b/installer/sashimono-install.sh @@ -490,7 +490,7 @@ rm -r "$tmp" -out $SASHIMONO_DATA/contract_template/cfg/tlscert.pem -subj "/C=HP/CN=$(jq -r '.hp.host_address' $SASHIMONO_DATA/sa.cfg)" # Install Sashimono agent binaries into sashimono bin dir. -cp "$script_dir"/{sagent,hpfs,user-cgcreate.sh,user-install.sh,user-uninstall.sh,docker-registry-uninstall.sh} $SASHIMONO_BIN +cp "$script_dir"/{sagent,hpfs,user-cgcreate.sh,user-install.sh,dns_evernode.sh,user-uninstall.sh,docker-registry-uninstall.sh} $SASHIMONO_BIN chmod -R +x $SASHIMONO_BIN # Setup tls certs used for contract instance websockets. diff --git a/src/conf.cpp b/src/conf.cpp index 0955f4a..f7f503d 100644 --- a/src/conf.cpp +++ b/src/conf.cpp @@ -116,6 +116,7 @@ namespace conf ctx.hpfs_exe_path = ctx.exe_dir + "/hpfs"; ctx.user_install_sh = ctx.exe_dir + "/user-install.sh"; + ctx.dns_evernode_sh = ctx.exe_dir + "/dns_evernode.sh.sh"; ctx.user_uninstall_sh = ctx.exe_dir + "/user-uninstall.sh"; ctx.socket_path = ctx.data_dir + "/sa.sock";