diff --git a/.gitignore b/.gitignore index eb5df43..6acb7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build .vscode v2ray_config/proxy v2ray_config/06_outbounds_proxy.json +aur-* diff --git a/CMakeLists.txt b/CMakeLists.txt index e67a15b..670a0b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,29 +1,37 @@ cmake_minimum_required(VERSION 3.10) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_BUILD_TYPE DEBUG) +# set(CMAKE_BUILD_TYPE RELEASE) + +project(cgproxy VERSION 4.0) + +find_package(Threads REQUIRED) +find_package(nlohmann_json REQUIRED) +include_directories(${PROJECT_SOURCE_DIR}) -project(cgproxy VERSION 3.8) add_executable(cgattach cgattach.cpp) +add_executable(cgproxyd cgproxyd.cpp) +add_executable(cgproxy cgproxy.cpp) +add_executable(cgnoproxy cgnoproxy.cpp) +target_link_libraries(cgproxyd Threads::Threads nlohmann_json::nlohmann_json) +target_link_libraries(cgproxy nlohmann_json::nlohmann_json) +target_link_libraries(cgnoproxy nlohmann_json::nlohmann_json) -install(TARGETS cgattach DESTINATION /usr/bin - PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE SETUID) -install(FILES cgproxy.sh DESTINATION /usr/bin - RENAME cgproxy - PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) -install(FILES cgnoproxy.sh DESTINATION /usr/bin - RENAME cgnoproxy - PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) -# install(FILES run_in_cgroup.sh DESTINATION /usr/bin -# RENAME run_in_cgroup -# PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +# add_executable(client_test socket_client_test.cpp) +# target_link_libraries(client_test nlohmann_json::nlohmann_json) -install(FILES cgproxy.service - DESTINATION /usr/lib/systemd/system/) -install(FILES cgproxy.conf - DESTINATION /etc/) -install(FILES cgroup-tproxy.sh - DESTINATION /usr/share/cgproxy/scripts/) - -install(FILES readme.md - DESTINATION /share/doc/cgproxy/) +set(basic_permission OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) + +install(TARGETS cgattach DESTINATION /usr/bin PERMISSIONS ${basic_permission}) +install(TARGETS cgproxyd DESTINATION /usr/bin PERMISSIONS ${basic_permission}) +install(TARGETS cgproxy DESTINATION /usr/bin PERMISSIONS ${basic_permission}) +install(TARGETS cgnoproxy DESTINATION /usr/bin PERMISSIONS ${basic_permission}) + +install(FILES cgproxy.service DESTINATION /usr/lib/systemd/system/) +install(FILES config.json DESTINATION /etc/cgproxy/) +install(FILES cgroup-tproxy.sh DESTINATION /usr/share/cgproxy/scripts/) +install(FILES readme.md DESTINATION /share/doc/cgproxy/) ## package for deb and rpm @@ -34,7 +42,7 @@ set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "cgproxy will transparent proxy anything r ## deb pack set(CPACK_DEBIAN_PACKAGE_NAME "cgproxy") set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "x86_64") -set(CPACK_DEBIAN_PACKAGE_DEPENDS "systemd") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "systemd" "nlohmann-json3-dev") set(CPACK_DEBIAN_PACKAGE_SECTION "network") set(CPACK_DEBIAN_PACKAGE_PRIORITY "Optional") set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/springzfx/cgproxy") @@ -44,7 +52,7 @@ set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CONTROL_DIR}/postinst;${CONTROL_DIR}/p ## rpm pack set(CPACK_RPM_PACKAGE_ARCHITECTURE, "x86_64") -set(CPACK_RPM_PACKAGE_REQUIRES "systemd") +set(CPACK_RPM_PACKAGE_REQUIRES "systemd" "json-devel") set(CPACK_RPM_PACKAGE_GROUP "network") set(CPACK_RPM_PACKAGE_URL "https://github.com/springzfx/cgproxy") set(CONTROL_DIR ${CMAKE_SOURCE_DIR}/control) diff --git a/aur-cgproxy-local/PKGBUILD b/aur-cgproxy-local/PKGBUILD deleted file mode 100644 index 0208cec..0000000 --- a/aur-cgproxy-local/PKGBUILD +++ /dev/null @@ -1,40 +0,0 @@ -# Maintainer: Fancy Zhang -pkgname=cgproxy-git -pkgver=v3.8.r1.gc0668fd -pkgrel=1 -pkgdesc="A transparent proxy program with cgroup2, like proxychains" -arch=('x86_64') -url="https://github.com/springzfx/cgproxy" -license=('') -groups=('') -makedepends=('cmake') -depends=('systemd') -provides=('cgproxy') -conflicts=('cgproxy') - -curr_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -source=("${pkgname}::git+file://${curr_dir}/../.git") -# source=("${pkgname}::git+file:///home/fancy/workspace/cgproxy/.git") -md5sums=('SKIP') - -pkgver() { - cd "$pkgname" - ( set -o pipefail - git describe --long --tags 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' || - printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" - ) -} - -backup=('etc/cgproxy.conf') -install="cgproxy.install" - -build(){ - cd "$pkgname" - mkdir -p build && cd build && cmake .. && make -} - -package_cgproxy-git(){ - cd "$pkgname"/build - make DESTDIR=$pkgdir install -} - diff --git a/aur-cgproxy-local/cgproxy.install b/aur-cgproxy-local/cgproxy.install deleted file mode 100644 index e6081e8..0000000 --- a/aur-cgproxy-local/cgproxy.install +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -post_install(){ -cat <<'DOC' - to start service: - systemctl enable --now cgproxy.service -DOC -} diff --git a/cgattach.cpp b/cgattach.cpp index 12c6f75..fe8b146 100644 --- a/cgattach.cpp +++ b/cgattach.cpp @@ -1,100 +1,23 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "cgroup_attach.hpp" using namespace std; - -#define error(...) {fprintf(stderr, __VA_ARGS__);fprintf(stderr, "\n");} -#define debug(...) {fprintf(stdout, __VA_ARGS__);fprintf(stdout, "\n");} - void print_usage() { fprintf(stdout, "usage: cgattach \n"); } -bool exist(string path) { - struct stat st; - if (stat(path.c_str(), &st) != -1) { - return S_ISDIR(st.st_mode); - } - return false; -} - -bool validate(string pid, string cgroup) { - bool pid_v = regex_match(pid, regex("^[0-9]+$")); - bool cg_v = regex_match(cgroup, regex("^\\/[a-zA-Z0-9\\-_./@]*$")); - if (pid_v && cg_v) - return true; - - error("paramater validate error"); - print_usage(); - exit(EXIT_FAILURE); -} - -string get_cgroup2_mount_point(){ - char cgroup2_mount_point[100]=""; - FILE* fp = popen("findmnt -t cgroup2 -n -o TARGET", "r"); - int count=fscanf(fp,"%s",&cgroup2_mount_point); - fclose(fp); - if (count=0){ - error("cgroup2 not supported"); - exit(EXIT_FAILURE); - } - return cgroup2_mount_point; -} - int main(int argc, char *argv[]) { int flag=setuid(0); if (flag!=0) { - perror("cgattach setuid"); + perror("cgattach need root"); exit(EXIT_FAILURE); } if (argc != 3) { - error("only need 2 paramaters"); + error("need exact 2 paramaters"); print_usage(); exit(EXIT_FAILURE); } string pid = string(argv[1]); string cgroup_target = string(argv[2]); - validate(pid, cgroup_target); - // string cgroup_mount_point = "/sys/fs/cgroup"; - string cgroup_mount_point = get_cgroup2_mount_point(); - string cgroup_target_path = cgroup_mount_point + cgroup_target; - string cgroup_target_procs = cgroup_target_path + "/cgroup.procs"; - // check if exist, we will create it if not exist - if (!exist(cgroup_target_path)) { - if (mkdir(cgroup_target_path.c_str(), - S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) == 0) { - debug("created cgroup %s success", cgroup_target.c_str()); - } else { - error("created cgroup %s failed, errno %d", cgroup_target.c_str(), errno); - exit(EXIT_FAILURE); - } - // error("cgroup %s not exist",cgroup_target.c_str()); - // exit(EXIT_FAILURE); - } - - // put pid to target cgroup - ofstream procs(cgroup_target_procs, ofstream::app); - if (!procs.is_open()) { - error("open file %s failed", cgroup_target_procs.c_str()); - exit(EXIT_FAILURE); - } - procs << pid.c_str() << endl; - procs.close(); - - // maybe there some write error, for example process pid may not exist - if (!procs) { - error("write %s to %s failed, maybe process %s not exist", - pid.c_str(), cgroup_target_procs.c_str(), pid.c_str()); - exit(EXIT_FAILURE); - } - return EXIT_SUCCESS; + CGPROXY::CGROUP::attach(pid,cgroup_target); } diff --git a/cgnoproxy.cpp b/cgnoproxy.cpp new file mode 100644 index 0000000..8177306 --- /dev/null +++ b/cgnoproxy.cpp @@ -0,0 +1,31 @@ +#include +#include "socket_client.hpp" +using json = nlohmann::json; +using namespace CGPROXY; + +bool attach2cgproxy(){ + pid_t pid=getpid(); + json j; + j["type"] = MSG_TYPE_NOPROXY_PID; + j["data"] = pid; + int status; + SOCKET::send(j.dump(), status); + return status==0; +} + +int main(int argc, char *argv[]){ + int shift=1; + if (argc==1){ + error("usage: cgnoproxy [--debug] \nexample: cgnoproxy curl -I https://www.google.com"); + exit(EXIT_FAILURE); + } + processArgs(argc,argv,shift); + + if (!attach2cgproxy()){ + error("attach process failed"); + exit(EXIT_FAILURE); + } + + string s=join2str(argc-shift,argv+shift,' '); + return system(s.c_str()); +} \ No newline at end of file diff --git a/cgnoproxy.sh b/cgnoproxy.sh deleted file mode 100644 index e66a182..0000000 --- a/cgnoproxy.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -config="/etc/cgproxy.conf" -source $config - -# test suid bit -if [ -u "$(which cgattach)" ]; then - cgattach $$ $cgroup_noproxy && attached=1 -else - sudo cgattach $$ $cgroup_noproxy && attached=1 -fi - -# test attach success or not -[[ -z "$attached" ]] && echo "attach error" && exit 1 - -exec "$@" \ No newline at end of file diff --git a/cgproxy.conf b/cgproxy.conf deleted file mode 100644 index c224b52..0000000 --- a/cgproxy.conf +++ /dev/null @@ -1,48 +0,0 @@ -## cgroup transparent proxy -## see how to configure, https://github.com/springzfx/cgproxy - -################################################################################### -## any process in cgroup_proxy will be proxied, and cgroup_noproxy the opposite -## note, cgroup must start with slash '/' -## the value can be string or bash array -## for array, only the first element will be created if not exist -## and the rest elements will not, so won't be applied if not exist - -### global proxy with v2ray service -# cgroup_proxy="/" -# cgroup_noproxy=("/noproxy.slice" "/system.slice/v2ray.service") - -### global proxy with manual `cgnoporxy qv2ray` -# cgroup_proxy="/" -# cgroup_noproxy="/noproxy.slice" - -### default -cgroup_proxy="/proxy.slice" -cgroup_noproxy="/noproxy.slice" - - -################################################################################### -## allow as gateway for local network -enable_gateway=false - - -################################################################################### -## listening port of another proxy process, for example v2ray -port=12345 - - -################################################################################### -## if you set to false, it's traffic won't go through proxy, -## but still can go direct to internet -enable_dns=true -enable_tcp=true -enable_udp=true -enable_ipv4=true -enable_ipv6=true - - -################################################################################### -## do not modify this if you don't known what you are doing -table=100 -fwmark=0x01 -mark_newin=0x02 diff --git a/cgproxy.cpp b/cgproxy.cpp new file mode 100644 index 0000000..867eee3 --- /dev/null +++ b/cgproxy.cpp @@ -0,0 +1,31 @@ +#include +#include "socket_client.hpp" +using json = nlohmann::json; +using namespace CGPROXY; + +bool attach2cgproxy(){ + pid_t pid=getpid(); + json j; + j["type"] = MSG_TYPE_PROXY_PID; + j["data"] = pid; + int status; + SOCKET::send(j.dump(), status); + return status==0; +} + +int main(int argc, char *argv[]){ + int shift=1; + if (argc==1){ + error("usage: cgproxy [--debug] \nexample: cgroxy curl -I https://www.google.com"); + exit(EXIT_FAILURE); + } + processArgs(argc,argv,shift); + + if (!attach2cgproxy()){ + error("attach process failed"); + exit(EXIT_FAILURE); + } + + string s=join2str(argc-shift,argv+shift,' '); + return system(s.c_str()); +} \ No newline at end of file diff --git a/cgproxy.service b/cgproxy.service index 521e857..fb040ec 100644 --- a/cgproxy.service +++ b/cgproxy.service @@ -1,11 +1,10 @@ [Unit] -Description=proxy cgroup +Description=cgproxy service After=network.target [Service] -ExecStart=sh /usr/share/cgproxy/scripts/cgroup-tproxy.sh --config=/etc/cgproxy.conf -ExecStop= sh /usr/share/cgproxy/scripts/cgroup-tproxy.sh stop -RemainAfterExit=1 +Type=simple +ExecStart=/usr/bin/cgproxyd [Install] WantedBy=multi-user.target diff --git a/cgproxy.sh b/cgproxy.sh deleted file mode 100644 index beeeb52..0000000 --- a/cgproxy.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -config="/etc/cgproxy.conf" -source $config - -# test suid bit -if [ -u "$(which cgattach)" ]; then - cgattach $$ $cgroup_proxy && attached=1 -else - sudo cgattach $$ $cgroup_proxy && attached=1 -fi - -# test attach success or not -[[ -z "$attached" ]] && echo "attach error" && exit 1 - -exec "$@" \ No newline at end of file diff --git a/cgproxyd.cpp b/cgproxyd.cpp new file mode 100644 index 0000000..65effaf --- /dev/null +++ b/cgproxyd.cpp @@ -0,0 +1,140 @@ +#include "common.hpp" +#include "socket_server.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.hpp" +#include "cgroup_attach.hpp" + +using namespace std; +using json = nlohmann::json; +using namespace CGPROXY::SOCKET; +using namespace CGPROXY::CONFIG; +using namespace CGPROXY::CGROUP; + +namespace CGPROXY{ + +class cgproxyd{ + thread_arg arg_t; + Config config; + pthread_t socket_thread_id = -1; + + static cgproxyd* instance; + static int handle_msg_static(char* msg){ + if (!instance) { + error("no cgproxyd instance assigned"); + return ERROR; + } + return instance->handle_msg(msg); + } + static void signalHandler( int signum ){ + debug("Signal %d received.", &signum); + if (!instance){ error("no cgproxyd instance assigned");} + else { instance->stop(); } + exit(signum); + } + + int handle_msg(char *msg) { + debug("received msg: %s", msg); + json j; + try{ j = json::parse(msg); }catch(exception& e){debug("msg paser error");return MSG_ERROR;} + + int type, status; + int pid, cgroup_target; + try { + type = j.at("type").get(); + switch (type) + { + case MSG_TYPE_JSON: + status=config.loadFromJson(j.at("data")); + if (status==SUCCESS) status=applyConfig(&config); + return status; + break; + case MSG_TYPE_CONFIG_PATH: + status=config.loadFromFile(j.at("data").get()); + if (status==SUCCESS) status=applyConfig(&config); + return status; + break; + case MSG_TYPE_PROXY_PID: + pid=j.at("data").get(); + status=attach(pid, config.cgroup_proxy_preserved); + return status; + break; + case MSG_TYPE_NOPROXY_PID: + pid=j.at("data").get(); + status=attach(pid, config.cgroup_noproxy_preserved); + return status; + break; + default: + return MSG_ERROR; + break; + }; + } catch (out_of_range &e) { + return MSG_ERROR; + } catch (exception &e){ + return ERROR; + } + } + + pthread_t startSocketListeningThread() { + arg_t.handle_msg = &handle_msg_static; + pthread_t thread_id; + int status = + pthread_create(&thread_id, NULL, &SocketServer::startThread, &arg_t); + if (status != 0) + error("socket thread create failed"); + return thread_id; + } + + void assignStaticInstance(){ + instance=this; + } + + public: + int start(int argc, char* argv[]) { + signal(SIGINT, &signalHandler); + signal(SIGTERM,&signalHandler); + signal(SIGHUP,&signalHandler); + + int shift=1; + processArgs(argc,argv,shift); + + config.loadFromFile(DEFAULT_CONFIG_FILE); + applyConfig(&config); + + assignStaticInstance(); + socket_thread_id = startSocketListeningThread(); + pthread_join(socket_thread_id, NULL); + return 0; + } + int applyConfig(Config *c) { + system(TPROXY_IPTABLS_CLEAN); + c->toEnv(); + system(TPROXY_IPTABLS_START); + // no need to track running status + return 0; + } + void stop(){ + debug("stopping"); + system(TPROXY_IPTABLS_CLEAN); + } + ~cgproxyd(){ + stop(); + } +}; + +cgproxyd* cgproxyd::instance=NULL; + +} + +int main(int argc, char* argv[]) { + CGPROXY::cgproxyd d; + return d.start(argc,argv); +} \ No newline at end of file diff --git a/cgroup-tproxy.sh b/cgroup-tproxy.sh index f419b89..575a756 100644 --- a/cgroup-tproxy.sh +++ b/cgroup-tproxy.sh @@ -30,29 +30,35 @@ cat << 'DOC' DOC } -check_root(){ - uid=$(id -u) - [ ! $uid -eq 0 ] && { >&2 echo "permission denied, need root";exit 0; } -} - -check_root +## check root +[ ! $(id -u) -eq 0 ] && { >&2 echo "need root to modify iptables";exit -1; } ## any process in this cgroup will be proxied -cgroup_proxy="/proxy.slice" -cgroup_noproxy="/noproxy.slice" +if [ -z ${cgroup_proxy+x} ]; then + cgroup_proxy="/proxy.slice" +else + IFS=':' read -r -a cgroup_proxy <<< "$cgroup_proxy" +fi + +## any process in this cgroup will not be proxied +if [ -z ${cgroup_noproxy+x} ]; then + cgroup_noproxy="/noproxy.slice" +else + IFS=':' read -r -a cgroup_noproxy <<< "$cgroup_noproxy" +fi # allow as gateway for local network -enable_gateway=false +[ -z ${enable_gateway+x} ] && enable_gateway=false ## some variables -port=12345 +[ -z ${port+x} ] && port=12345 ## some options -enable_dns=true -enable_tcp=true -enable_udp=true -enable_ipv4=true -enable_ipv6=true +[ -z ${enable_dns+x} ] && enable_dns=true +[ -z ${enable_tcp+x} ] && enable_tcp=true +[ -z ${enable_udp+x} ] && enable_udp=true +[ -z ${enable_ipv4+x} ] && enable_ipv4=true +[ -z ${enable_ipv6+x} ] && enable_ipv6=true ## do not modify this if you don't known what you are doing table=100 @@ -64,35 +70,41 @@ cgroup_mount_point=$(findmnt -t cgroup2 -n -o TARGET) cgroup_type="cgroup2" cgroup_procs_file="cgroup.procs" + +stop(){ + iptables -t mangle -L TPROXY_PRE &> /dev/null || return + echo "cleaning tproxy iptables" + iptables -t mangle -D PREROUTING -j TPROXY_PRE + iptables -t mangle -D OUTPUT -j TPROXY_OUT + iptables -t mangle -F TPROXY_PRE + iptables -t mangle -F TPROXY_OUT + iptables -t mangle -F TPROXY_ENT + iptables -t mangle -X TPROXY_PRE + iptables -t mangle -X TPROXY_OUT + iptables -t mangle -X TPROXY_ENT + ip6tables -t mangle -D PREROUTING -j TPROXY_PRE + ip6tables -t mangle -D OUTPUT -j TPROXY_OUT + ip6tables -t mangle -F TPROXY_PRE + ip6tables -t mangle -F TPROXY_OUT + ip6tables -t mangle -F TPROXY_ENT + ip6tables -t mangle -X TPROXY_PRE + ip6tables -t mangle -X TPROXY_OUT + ip6tables -t mangle -X TPROXY_ENT + ip rule delete fwmark $fwmark lookup $table + ip route flush table $table + ip -6 rule delete fwmark $fwmark lookup $table + ip -6 route flush table $table + ## may not exist, just ignore, and tracking their existence is not reliable + iptables -t nat -D POSTROUTING -m owner ! --socket-exists -j MASQUERADE &> /dev/null + ip6tables -t nat -D POSTROUTING -m owner ! --socket-exists -s fc00::/7 -j MASQUERADE &> /dev/null +} + ## parse parameter for i in "$@" do case $i in stop) - echo "stopping tproxy iptables" - iptables -t mangle -D PREROUTING -j TPROXY_PRE - iptables -t mangle -D OUTPUT -j TPROXY_OUT - iptables -t mangle -F TPROXY_PRE - iptables -t mangle -F TPROXY_OUT - iptables -t mangle -F TPROXY_ENT - iptables -t mangle -X TPROXY_PRE - iptables -t mangle -X TPROXY_OUT - iptables -t mangle -X TPROXY_ENT - ip6tables -t mangle -D PREROUTING -j TPROXY_PRE - ip6tables -t mangle -D OUTPUT -j TPROXY_OUT - ip6tables -t mangle -F TPROXY_PRE - ip6tables -t mangle -F TPROXY_OUT - ip6tables -t mangle -F TPROXY_ENT - ip6tables -t mangle -X TPROXY_PRE - ip6tables -t mangle -X TPROXY_OUT - ip6tables -t mangle -X TPROXY_ENT - ip rule delete fwmark $fwmark lookup $table - ip route flush table $table - ip -6 rule delete fwmark $fwmark lookup $table - ip -6 route flush table $table - ## may not exist, just ignore, and tracking their existence is not reliable - iptables -t nat -D POSTROUTING -m owner ! --socket-exists -j MASQUERADE &> /dev/null - ip6tables -t nat -D POSTROUTING -m owner ! --socket-exists -s fc00::/7 -j MASQUERADE &> /dev/null + stop exit 0 ;; --config=*) @@ -111,6 +123,8 @@ done test -d $cgroup_mount_point$cgroup_proxy || mkdir $cgroup_mount_point$cgroup_proxy || exit -1; test -d $cgroup_mount_point$cgroup_noproxy || mkdir $cgroup_mount_point$cgroup_noproxy || exit -1; + +echo "applying tproxy iptables" ## use TPROXY #ipv4# ip rule add fwmark $fwmark table $table diff --git a/cgroup_attach.hpp b/cgroup_attach.hpp new file mode 100644 index 0000000..06fdeb5 --- /dev/null +++ b/cgroup_attach.hpp @@ -0,0 +1,103 @@ +#ifndef CGPROUP_ATTACH_H +#define CGPROUP_ATTACH_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common.hpp" +using namespace std; + +namespace CGPROXY::CGROUP{ + +bool exist(string path) { + struct stat st; + if (stat(path.c_str(), &st) != -1) { + return S_ISDIR(st.st_mode); + } + return false; +} + +bool validate(string pid, string cgroup) { + bool pid_v = validPid(pid); + bool cg_v = validCgroup(cgroup); + if (pid_v && cg_v) + return true; + + error("attach paramater validate error"); + return_error +} + +string get_cgroup2_mount_point(int &status){ + char cgroup2_mount_point[100]=""; + FILE* fp = popen("findmnt -t cgroup2 -n -o TARGET", "r"); + int count=fscanf(fp,"%s",&cgroup2_mount_point); + fclose(fp); + if (count=0){ + error("cgroup2 not supported"); + status=-1; + return NULL; + } + status=0; + return cgroup2_mount_point; +} + +int attach(const string pid, const string cgroup_target) { + if (getuid()!=0) { + error("need root to attach cgroup"); + return_error + } + + debug("attaching %s to %s",pid.c_str(),cgroup_target.c_str()); + + int status; + if (!validate(pid, cgroup_target)) return_error + string cgroup_mount_point = get_cgroup2_mount_point(status); + if (status!=0) return_error + string cgroup_target_path = cgroup_mount_point + cgroup_target; + string cgroup_target_procs = cgroup_target_path + "/cgroup.procs"; + + // check if exist, we will create it if not exist + if (!exist(cgroup_target_path)) { + if (mkdir(cgroup_target_path.c_str(), + S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) == 0) { + debug("created cgroup %s success", cgroup_target.c_str()); + } else { + error("created cgroup %s failed, errno %d", cgroup_target.c_str(), errno); + return_error + } + // error("cgroup %s not exist",cgroup_target.c_str()); + // return_error + } + + // put pid to target cgroup + ofstream procs(cgroup_target_procs, ofstream::app); + if (!procs.is_open()) { + error("open file %s failed", cgroup_target_procs.c_str()); + return_error + } + procs << pid.c_str() << endl; + procs.close(); + + // maybe there some write error, for example process pid may not exist + if (!procs) { + error("write %s to %s failed, maybe process %s not exist", + pid.c_str(), cgroup_target_procs.c_str(), pid.c_str()); + return_error + } + return_success +} + +int attach(const int pid, const string cgroup_target){ + return attach(to_str(pid), cgroup_target); +} + +} + +#endif \ No newline at end of file diff --git a/common.hpp b/common.hpp new file mode 100644 index 0000000..07fe998 --- /dev/null +++ b/common.hpp @@ -0,0 +1,107 @@ +#ifndef COMMON_H +#define COMMON_H 1 + +#define TPROXY_IPTABLS_START "sh /usr/share/cgproxy/scripts/cgroup-tproxy.sh" +#define TPROXY_IPTABLS_CLEAN "sh /usr/share/cgproxy/scripts/cgroup-tproxy.sh stop" + +#define SOCKET_PATH "/tmp/cgproxy_unix_socket" +#define LISTEN_BACKLOG 64 +#define DEFAULT_CONFIG_FILE "/etc/cgproxy/config.json" + +#define CGROUP_PROXY_PRESVERED "/proxy.slice" +#define CGROUP_NOPROXY_PRESVERED "/noproxy.slice" + +#define MSG_TYPE_JSON 1 +#define MSG_TYPE_CONFIG_PATH 2 +#define MSG_TYPE_PROXY_PID 3 +#define MSG_TYPE_NOPROXY_PID 4 + +#define UNKNOWN_ERROR 99 +#define ERROR -1 +#define SUCCESS 0 +#define CONN_ERROR 1 +#define MSG_ERROR 2 +#define PARSE_ERROR 3 +#define PARAM_ERROR 4 +#define APPLY_ERROR 5 +#define CGROUP_ERROR 6 +#define FILE_ERROR 7 + + +#include +#include +#include +#include +using namespace std; + +static bool enable_debug=false; +static bool print_help=false; + +#define error(...) {fprintf(stderr, __VA_ARGS__);fprintf(stderr, "\n");} +#define debug(...) if (enable_debug) {fprintf(stdout, __VA_ARGS__);fprintf(stdout, "\n");} +#define return_error return -1; +#define return_success return 0; + + +void processArgs(const int argc, char *argv[], int &shift){ + for (int i=1;i +string to_str(T... args) { + stringstream ss; + ss.clear(); + ss << std::boolalpha; + (ss << ... << args); + return ss.str(); +} + +string join2str(const vector t, const char delm=' '){ + string s; + for (const auto &e : t) + e!=*(t.end()-1)?s+=e+delm:s+=e; + return s; +} + +string join2str(const int argc,char** argv, const char delm=' '){ + string s; + for (int i=0;i cgroup){ + for (auto &e:cgroup){ + if (!regex_match(e, regex("^/[a-zA-Z0-9\\-_./@]*$"))){ + return false; + } + } + return true; +} + +bool validPid(const string pid){ + return regex_match(pid, regex("^[0-9]+$")); +} + +bool validPort(const int port){ + return port>0; +} + +#endif \ No newline at end of file diff --git a/config.hpp b/config.hpp new file mode 100644 index 0000000..2e8fd48 --- /dev/null +++ b/config.hpp @@ -0,0 +1,143 @@ +#ifndef CONFIG_H +#define CONFIG_H +#include "common.hpp" +#include "socket_server.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std; +using json = nlohmann::json; + +namespace CGPROXY::CONFIG{ + +struct Config { + public: + const string cgroup_proxy_preserved=CGROUP_PROXY_PRESVERED; + const string cgroup_noproxy_preserved=CGROUP_NOPROXY_PRESVERED; + private: + vector cgroup_proxy; + vector cgroup_noproxy; + bool enable_gateway = false; + int port = 12345; + bool enable_dns = true; + bool enable_tcp = true; + bool enable_udp = true; + bool enable_ipv4 = true; + bool enable_ipv6 = true; + +public: + void toEnv() { + mergeReserved(); + setenv("cgroup_proxy", join2str(cgroup_proxy,':').c_str(), 1); + setenv("cgroup_noproxy", join2str(cgroup_noproxy, ':').c_str(), 1); + setenv("enable_gateway", to_str(enable_gateway).c_str(), 1); + setenv("port", to_str(port).c_str(), 1); + setenv("enable_dns", to_str(enable_dns).c_str(), 1); + setenv("enable_tcp", to_str(enable_tcp).c_str(), 1); + setenv("enable_udp", to_str(enable_udp).c_str(), 1); + setenv("enable_ipv4", to_str(enable_ipv4).c_str(), 1); + setenv("enable_ipv6", to_str(enable_ipv6).c_str(), 1); + } + + int saveToFile(const string f){ + ofstream o(f); + if (!o.is_open()) return FILE_ERROR; + json j=toJson(); + o << setw(4) << j << endl; + o.close(); + return 0; + } + + json toJson(){ + json j; + #define add2json(v) j[#v]=v; + add2json(cgroup_proxy); + add2json(cgroup_noproxy); + add2json(enable_gateway); + add2json(port); + add2json(enable_dns); + add2json(enable_tcp); + add2json(enable_udp); + add2json(enable_ipv4); + add2json(enable_ipv6); + #undef add2json + return j; + } + + int loadFromFile(const string f) { + debug("loading config: %s", f.c_str()); + ifstream ifs(f); + if (ifs.is_open()){ + json j; + try { ifs >> j; }catch (exception& e){error("parse error: %s", f.c_str());ifs.close();return PARSE_ERROR;} + ifs.close(); + return loadFromJson(j); + }else{ + error("open failed: %s",f.c_str()); + return FILE_ERROR; + } + } + + int loadFromJson(const json &j) { + if (!validateJson(j)) {error("json validate fail"); return PARAM_ERROR;} + #define tryassign(v) try{j.at(#v).get_to(v);}catch(exception &e){} + tryassign(cgroup_proxy); + tryassign(cgroup_noproxy); + tryassign(enable_gateway); + tryassign(port); + tryassign(enable_dns); + tryassign(enable_tcp); + tryassign(enable_udp); + tryassign(enable_ipv4); + tryassign(enable_ipv6); + #undef assign + return 0; + } + + void mergeReserved(){ + #define merge(v) { \ + v.erase(std::remove(v.begin(), v.end(), v ## _preserved), v.end()); \ + v.insert(v.begin(), v ## _preserved); \ + } + merge(cgroup_proxy); + merge(cgroup_noproxy); + #undef merge + + } + + bool validateJson(const json &j){ + bool status=true; + const set boolset={"enable_gateway","enable_dns","enable_tcp","enable_udp","enable_ipv4","enable_ipv6"}; + for (auto& [key, value] : j.items()) { + if (key=="cgroup_proxy"||key=="cgroup_noproxy"){ + if (value.is_string()&&!validCgroup((string)value)) status=false; + // TODO what if vector etc. + if (value.is_array()&&!validCgroup((vector)value)) status=false; + if (!value.is_string()&&!value.is_array()) status=false; + }else if (key=="port"){ + if (!validPort(value)) status=false; + }else if (boolset.find(key)!=boolset.end()){ + if (!value.is_boolean()) status=false; + }else{ + error("unknown key: %s", key.c_str()); + return false; + } + if (!status) { + error("invalid value for key: %s", key.c_str()); + return false; + } + } + return true; + } +}; + +} +#endif \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..5fd161b --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "cgroup_noproxy": ["/system.slice/v2ray.service"], + "cgroup_proxy": [], + "enable_dns": true, + "enable_gateway": false, + "enable_ipv4": true, + "enable_ipv6": true, + "enable_tcp": true, + "enable_udp": true, + "port": 12345 +} diff --git a/readme.md b/readme.md index f282d37..8a1a3c4 100644 --- a/readme.md +++ b/readme.md @@ -11,12 +11,12 @@ cgproxy will transparent proxy anything running in specific cgroup. It resembles It aslo supports global transparent proxy and gateway proxy. See [Global transparent proxy](#global-transparent-proxy) and [Gateway proxy](#gateway-proxy). - * [Transparent Proxy with cgroup v2](#transparent-proxy-with-cgroup-v2) * [Introduction](#introduction) * [Prerequest](#prerequest) * [How to install](#how-to-install) - * [How to use](#how-to-use) + * [Default usage](#default-usage) + * [Configuration](#configuration) * [Global transparent proxy](#global-transparent-proxy) * [Gateway proxy](#gateway-proxy) * [Other useful tools provided in this project](#other-useful-tools-provided-in-this-project) @@ -24,7 +24,7 @@ It aslo supports global transparent proxy and gateway proxy. See [Global transpa * [TIPS](#tips) * [Licences](#licences) - + @@ -43,14 +43,14 @@ It aslo supports global transparent proxy and gateway proxy. See [Global transpa ## How to install ```bash -mkdir build && cd build && cmake .. && make && sudo make install +mkdir build && cd build && cmake .. && make && make install ``` - It is alreay in [archlinux AUR](https://aur.archlinux.org/packages/?K=cgproxy). - DEB and RPM are packaged in [release page](https://github.com/springzfx/cgproxy/releases). -## How to use +## Default usage - First enable and start service @@ -60,72 +60,51 @@ mkdir build && cd build && cmake .. && make && sudo make install - Then prefix with cgproxy with your command, just like proxychains - ``` - cgproxy + ```bash + cgproxy [--debug] ``` - For example, test proxy ```bash - cgproxy curl -vIs https://www.google.com + cgproxy curl -I https://www.google.com ``` - To completely stop ``` sudo systemctl disable --now cgproxy.service ``` ----- -
- More config in /etc/cgproxy.conf (click to expand) -```bash -################################################################################### -## any process in cgroup_proxy will be proxied, and cgroup_noproxy the opposite -## note, cgroup must start with slash '/' -## the value can be string or bash array -## for array, only the first element will be created if not exist -## and the rest elements will not, so won't be applied if not exist +## Configuration -### global proxy with v2ray service -# cgroup_proxy="/" -# cgroup_noproxy=("/noproxy.slice" "/system.slice/v2ray.service") +Config file: **/etc/cgproxy/config.json** -### global proxy with manual `cgnoporxy qv2ray` -# cgroup_proxy="/" -# cgroup_noproxy="/noproxy.slice" - -### default -cgroup_proxy="/proxy.slice" -cgroup_noproxy="/noproxy.slice" - - -################################################################################### -## allow as gateway for local network -enable_gateway=false - - -################################################################################### -## listening port of another proxy process, for example v2ray -port=12345 - - -################################################################################### -## if you set to false, it's traffic won't go through proxy, -## but still can go direct to internet -enable_dns=true -enable_tcp=true -enable_udp=true -enable_ipv4=true -enable_ipv6=true - - -################################################################################### -## do not modify this if you don't known what you are doing -table=100 -fwmark=0x01 -mark_newin=0x02 +```json +{ + "cgroup_noproxy": ["/system.slice/v2ray.service"], + "cgroup_proxy": [], + "enable_dns": true, + "enable_gateway": false, + "enable_ipv4": true, + "enable_ipv6": true, + "enable_tcp": true, + "enable_udp": true, + "port": 12345 +} ``` -
+ +- **port** tproxy listenning port +- **cgroup_noproxy** cgroup array that no need to proxy, `/noproxy.slice` is preserved +- **cgroup_proxy** cgroup array that need to proxy, `/proxy.slice` is preserved +- **enable_gateway** enable gateway proxy for local devices +- **enable_dns** enable dns to go to proxy +- **enable_tcp** +- **enable_udp** +- **enable_ipv4** +- **enable_ipv6** + +**Note**: cgroup in configuration need to be exist, otherwise ignored + If you changed config, remember to restart service ```bash @@ -134,45 +113,34 @@ sudo systemctl restart cgproxy.service ## Global transparent proxy -- Set `cgroup_proxy="/"` in */etc/cgproxy.conf*, this will proxy all connection +- Set `"cgroup_proxy":["/"]` in configuration, this will proxy all connection -- And allow your proxy program (v2ray) direct to internet, two ways: - - active way - - run `cgnoproxy ` - - example: `cgnoproxy sudo v2ray -config config_file` - - example: `cgnoproxy qv2ray` - - - passive way, useful if you run v2ray as service +- Allow your proxy program (v2ray) direct to internet to avoid loop. Two ways: - set `cgroup_noproxy=""` + - active way, run command + + example: `cgnoproxy sudo v2ray -config config_file` + + example: `cgnoproxy qv2ray` + + - passive way, set it's cgroup in configuration, very useful for service - example: `cgroup_noproxy=("/noproxy.slice" "/system.slice/v2ray.service")` + example: `"cgroup_noproxy":["/system.slice/v2ray.service"]` - Finally, restart cgproxy service, that's all ## Gateway proxy -- Set `enable_gateway=true` in */etc/cgproxy.conf* -- And allow your proxy software (v2ray) direct to internet, described above -- Other device set this host as gateway, and set public dns if necessary +- Set `"enable_gateway":true` in configuration +- And allow your proxy software (v2ray) direct to internet if necessary, described above +- Other device set this host as gateway, and set public dns if need ## Other useful tools provided in this project - `cgnoproxy` run program wihout proxy, very useful in global transparent proxy ```bash - cgnoproxy - ``` - -- `run_in_cgroup` run command in specific cgroup which will create if not exist , cgroup can be only one level down exist cgroup, otherwise created fail. - - ```bash - run_in_cgroup --cgroup=CGROUP - # example - run_in_cgroup --cgroup=/mycgroup.slice ping 127.0.0.1 + cgnoproxy [--debug] ``` - `cgattach` attach specific process pid to specific cgroup which will create if not exist , cgroup can be only one level down exist cgroup, otherwise created fail. @@ -185,8 +153,6 @@ sudo systemctl restart cgproxy.service ## NOTES -- `cgattach` has *suid* bit set by default, be careful to use on multi-user server for securiry. To avoid this situation, you can remove the *suid* bit , then it will fallback to use *sudo*, with *sudoer* you can restrict permission or set NOPASSWD for youself. - - v2ray TPROXY need root or special permission ```bash diff --git a/run_in_cgroup.sh b/run_in_cgroup.sh deleted file mode 100644 index f15c900..0000000 --- a/run_in_cgroup.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -print_help(){ -cat << 'DOC' -usage: - run_in_cgroup --cgroup=CGROUP - run_in_cgroup --help -note: - CGROUP must start will slash '/' , and no special character -example: - run_in_cgroup --cggroup=/mycgroup.slice ping 127.0.0.1 -DOC -} - -## parse parameter -for i in "$@" -do -case $i in - --cgroup=*) - cgroup=${i#*=} - shift - ;; - --help) - print_help - exit 0 - shift - ;; - -*) - shift - ;; - *) - break - ;; -esac -done - -[[ -z "$cgroup" ]] && print_help && exit 1 -[[ -z "$@" ]] && print_help && exit 1 - -# test suid bit -if [ -u "$(which cgattach)" ]; then - cgattach $$ $cgroup && attached=1 -else - sudo cgattach $$ $cgroup && attached=1 -fi - -# test attach success or not -[[ -z "$attached" ]] && print_help && exit 1 - -exec "$@" diff --git a/socket_client.hpp b/socket_client.hpp new file mode 100644 index 0000000..ce92128 --- /dev/null +++ b/socket_client.hpp @@ -0,0 +1,64 @@ +#ifndef SOCKET_CLIENT_H +#define SOCKET_CLIENT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common.hpp" + +using namespace std; + +namespace CGPROXY::SOCKET{ + +#define return_if_error(flag, msg) \ + if (flag == -1) { \ + perror(msg); \ + status = CONN_ERROR; \ + close(sfd); \ + return; \ + } + +void send(const char *msg, int &status) { + debug("send msg: %s", msg); + status = UNKNOWN_ERROR; + + int flag; + int sfd = socket(AF_UNIX, SOCK_STREAM, 0); + + struct sockaddr_un unix_socket; + memset(&unix_socket, '\0', sizeof(struct sockaddr_un)); + unix_socket.sun_family = AF_UNIX; + strncpy(unix_socket.sun_path, SOCKET_PATH, sizeof(unix_socket.sun_path) - 1); + + flag = + connect(sfd, (struct sockaddr *)&unix_socket, sizeof(struct sockaddr_un)); + return_if_error(flag, "connect"); + + int msg_len = strlen(msg); + flag = write(sfd, &msg_len, sizeof(int)); + return_if_error(flag, "write length"); + flag = write(sfd, msg, msg_len * sizeof(char)); + return_if_error(flag, "write msg"); + + flag = read(sfd, &status, sizeof(int)); + return_if_error(flag, "read return value"); + + close(sfd); +} + +void send(const string msg, int &status) { + int msg_len = msg.length(); + char buff[msg_len]; + msg.copy(buff, msg_len, 0); + buff[msg_len] = '\0'; + send(buff, status); + debug("return status: %d", status); +} + +} +#endif \ No newline at end of file diff --git a/socket_server.hpp b/socket_server.hpp new file mode 100644 index 0000000..a37a6f3 --- /dev/null +++ b/socket_server.hpp @@ -0,0 +1,94 @@ +#ifndef SOCKET_SERVER_H +#define SOCKET_SERVER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common.hpp" +using namespace std; +namespace fs = std::filesystem; + +namespace CGPROXY::SOCKET{ + +#define continue_if_error(flag, msg) \ + if (flag == -1) { \ + perror(msg); \ + continue; \ + } + +struct thread_arg { + function handle_msg; +}; + +class SocketServer { +public: + int sfd = -1, cfd = -1, flag = -1; + struct sockaddr_un unix_socket; + + void socketListening(function callback) { + debug("starting socket listening"); + sfd = socket(AF_UNIX, SOCK_STREAM, 0); + + if (fs::exists(SOCKET_PATH)&&unlink(SOCKET_PATH)==-1){ + error("%s exist, and can't unlink",SOCKET_PATH); + return; + } + memset(&unix_socket, '\0', sizeof(struct sockaddr_un)); + unix_socket.sun_family = AF_UNIX; + strncpy(unix_socket.sun_path, SOCKET_PATH, + sizeof(unix_socket.sun_path) - 1); + + bind(sfd, (struct sockaddr *)&unix_socket, sizeof(struct sockaddr_un)); + + listen(sfd, LISTEN_BACKLOG); + chmod(SOCKET_PATH,S_IRWXU|S_IRWXG|S_IRWXO); + + while (true) { + close(cfd); + cfd = accept(sfd, NULL, NULL); + continue_if_error(cfd, "accept"); + debug("accept connection: %d", cfd); + + // read length + int msg_len; + flag = read(cfd, &msg_len, sizeof(int)); + continue_if_error(flag, "read length"); + // read msg + char msg[msg_len]; + flag = read(cfd, msg, msg_len * sizeof(char)); + continue_if_error(flag, "read msg"); + msg[msg_len]='\0'; + // handle msg + int status = callback(msg); + // send back flag + flag = write(cfd, &status, sizeof(int)); + continue_if_error(flag, "write back"); + } + } + + ~SocketServer() { + close(sfd); + close(cfd); + unlink(SOCKET_PATH); + } + + static void *startThread(void *arg) { + thread_arg *p = (thread_arg *)arg; + SocketServer server; + server.socketListening(p->handle_msg); + return (void *)0; + } +}; + +} + +#endif \ No newline at end of file