From 2cbdc6e8458c237a4bc2585e2495390add78fa3a Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Sun, 23 Aug 2026 15:40:26 +0300 Subject: [PATCH] Add an OpenWrt package 3proxy is not in the OpenWrt feed, and the static binaries built here install nowhere in particular: they carry no service definition, no configuration and no way to survive a sysupgrade. Add the package as it would be submitted to openwrt/packages - a Makefile, a procd init script and a UCI schema. Built in the 24.10 SDK for mipsel_24kc against the distribution's libraries, so the result is a 220kB package depending on libopenssl and libpcre2 rather than a static binary, with TLS and PCRE support actually present. 3proxy.cfg is order dependent, which is the whole difficulty of generating it from UCI, where sections are unordered: - access rules are named sections referenced by a service through an ordered list, so the reference order decides precedence, and the list is flushed before each service so rules do not leak into the next one - parent proxies extend the allow rule they follow, so they hang off the access rule rather than the service - the TLS switches apply to every service below them, so the generator tracks what is in effect and emits a directive only when a service needs a different state, rather than letting certificate spoofing leak into a service that did not ask for it - bandwidth, connection and counter limits are global and match on their own ACL pattern, so they are a separate ordered list Nothing is enabled by default: the global switch is off, no service ships enabled, and a service section that omits the option does not start either, so installing the package opens no ports. Values that would make 3proxy reject the whole configuration at boot are checked while writing it - unknown access actions, limiter types, pcre types and actions - and reported instead of being passed through. --- scripts/openwrt/Makefile | 66 ++++ scripts/openwrt/files/3proxy.config | 59 ++++ scripts/openwrt/files/3proxy.init | 460 ++++++++++++++++++++++++++++ 3 files changed, 585 insertions(+) create mode 100644 scripts/openwrt/Makefile create mode 100644 scripts/openwrt/files/3proxy.config create mode 100644 scripts/openwrt/files/3proxy.init diff --git a/scripts/openwrt/Makefile b/scripts/openwrt/Makefile new file mode 100644 index 0000000..a8ae354 --- /dev/null +++ b/scripts/openwrt/Makefile @@ -0,0 +1,66 @@ +# +# Copyright (C) 2026 3proxy.org +# +# This is free software, licensed under the BSD 3-Clause License. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=3proxy +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +# A trailing ? tells the download helper the URL is complete and PKG_SOURCE +# must not be appended to it. +PKG_SOURCE_URL:=https://codeload.github.com/3proxy/3proxy/tar.gz/refs/tags/$(PKG_VERSION)? +PKG_HASH:=35b07de1046f3aaeac4a7085101b7e5c453efa3527cbdc42a84690366c7ecfa8 + +PKG_MAINTAINER:=Vladimir Dubrovin +PKG_LICENSE:=BSD-3-Clause +PKG_LICENSE_FILES:=copying +PKG_CPE_ID:=cpe:/a:3proxy:3proxy + +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/3proxy + SECTION:=net + CATEGORY:=Network + SUBMENU:=Web Servers/Proxies + TITLE:=tiny free proxy server + URL:=https://3proxy.org/ + DEPENDS:=+libopenssl +libpcre2 +endef + +define Package/3proxy/description + 3proxy is a tiny free proxy server supporting HTTP, HTTPS, FTP, SOCKS v4/v4a/v5, + POP3, SMTP, IMAP, TCP and UDP port mapping, with access control, bandwidth + limiting and traffic accounting. +endef + +define Package/3proxy/conffiles +/etc/config/3proxy +endef + +# Makefile.Linux appends to CFLAGS and LDFLAGS internally; the target flags have +# to be added rather than substituted, or the defines it relies on are lost. +define Build/Compile + $(MAKE) -C $(PKG_BUILD_DIR) -f Makefile.Linux \ + CC="$(TARGET_CC)" \ + EXTRA_CFLAGS="$(TARGET_CFLAGS) $(TARGET_CPPFLAGS)" \ + EXTRA_LDFLAGS="$(TARGET_LDFLAGS)" \ + PLUGINS= +endef + +define Package/3proxy/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/bin/3proxy $(1)/usr/bin/3proxy + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/3proxy.config $(1)/etc/config/3proxy + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/3proxy.init $(1)/etc/init.d/3proxy +endef + +$(eval $(call BuildPackage,3proxy)) diff --git a/scripts/openwrt/files/3proxy.config b/scripts/openwrt/files/3proxy.config new file mode 100644 index 0000000..f8bf232 --- /dev/null +++ b/scripts/openwrt/files/3proxy.config @@ -0,0 +1,59 @@ +config 3proxy 'global' + option enabled '0' + option nscache '65536' +# option nscache6 '65536' + # static records, added to the cache; 0.0.0.0 blackholes a name +# list nsrecord 'ads.example.com 0.0.0.0' + option maxconn '128' + option auth 'iponly' + option log 'syslog' +# option timeouts '1 5 30 60 180 1800 15 60 15 5 5' +# list include '/etc/3proxy/extra.cfg' + list nserver '8.8.8.8' + list nserver '8.8.4.4' +# list user 'admin:CL:password' +# list extra_config 'timeouts 1 5 30 60 180 1800 15 60' + # access list used by services which do not define their own + list acl 'lan' + +# Access rules are named sections referenced by services. The order of the +# references decides precedence: 3proxy stops at the first rule that matches. +config acl 'lan' + option action 'allow' + option src '192.168.1.0/24' + +config acl 'deny_private' + option action 'deny' + option dst '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16' + +#config acl 'via_upstream' +# option action 'allow' +# list parent 'upstream' + +# Parent proxies extend an allow rule to build a chain. Weights group them: +# parents whose weights sum to 1000 form one group and one is picked at random, +# several groups are chained in order. +#config parent 'upstream' +# option weight '1000' +# option type 'socks5' +# option ip '10.0.0.1' +# option port '1080' +# option username '' +# option password '' + +config service 'proxy' + option enabled '0' + option type 'proxy' + option port '3128' +# option bind '' +# option external '' +# option extra '' +# option auth 'strong' + list acl 'deny_private' + list acl 'lan' + +config service 'socks' + option enabled '0' + option type 'socks' + option port '1080' + list acl 'lan' diff --git a/scripts/openwrt/files/3proxy.init b/scripts/openwrt/files/3proxy.init new file mode 100644 index 0000000..b7a91c1 --- /dev/null +++ b/scripts/openwrt/files/3proxy.init @@ -0,0 +1,460 @@ +#!/bin/sh /etc/rc.common + +START=50 +USE_PROCD=1 + +CFGFILE=/var/etc/3proxy.cfg +PROG=/usr/bin/3proxy + +# 3proxy.cfg is order dependent: authentication and access rules apply to the +# service lines that follow them, and the access list has to be flushed before +# each service. The file is written as one global block followed by one block +# per service. + +acl_written=0 + +append_line() { + echo "$1" >> "$CFGFILE" +} + +append_include() { + echo "include $1" >> "$CFGFILE" +} + +# The limiter directives carry their own ACL pattern rather than attaching to a +# preceding allow rule, and 3proxy defaults every omitted field to *, so the +# trailing wildcards are dropped again to keep the file readable. +# logformat takes a single argument, so a format containing spaces has to be +# quoted. Quotes already present in the UCI value are not doubled. +append_logformat() { + local fmt="$1" + + case "$fmt" in + '"'*'"') ;; + *) fmt="\"$fmt\"" ;; + esac + + echo "logformat $fmt" >> "$CFGFILE" +} + +limit_match() { + local users src dst ports ops weekdays periods out + + config_get users "$1" users '*' + config_get src "$1" src '*' + config_get dst "$1" dst '*' + config_get ports "$1" ports '*' + config_get ops "$1" operations '*' + config_get weekdays "$1" weekdays '*' + config_get periods "$1" timeperiods '*' + + out="$users $src $dst $ports $ops $weekdays $periods" + while [ "${out% \*}" != "$out" ]; do out="${out% \*}"; done + + echo "$out" +} + +append_limit() { + local type rate period number count_type limit match + + config_get type "$1" type + + case "$type" in + bandlimin|bandlimout|nobandlimin|nobandlimout|\ + connlim|noconnlim|\ + countin|countout|countall|nocountin|nocountout|nocountall) ;; + *) + echo "3proxy: limit '$1' has unknown type '$type', ignored" >&2 + return 0 + ;; + esac + + match=$(limit_match "$1") + + case "$type" in + bandlimin|bandlimout) + config_get rate "$1" rate + [ -n "$rate" ] || { + echo "3proxy: limit '$1' needs a rate, ignored" >&2 + return 0 + } + echo "$type $rate $match" >> "$CFGFILE" + ;; + connlim) + config_get rate "$1" rate + config_get period "$1" period 0 + [ -n "$rate" ] || { + echo "3proxy: limit '$1' needs a rate, ignored" >&2 + return 0 + } + echo "$type $rate $period $match" >> "$CFGFILE" + ;; + countin|countout|countall) + config_get number "$1" number + config_get count_type "$1" count_type + config_get limit "$1" limit + [ -n "$number" ] && [ -n "$count_type" ] && [ -n "$limit" ] || { + echo "3proxy: limit '$1' needs number, count_type and limit, ignored" >&2 + return 0 + } + echo "$type $number $count_type $limit $match" >> "$CFGFILE" + ;; + *) + echo "$type $match" >> "$CFGFILE" + ;; + esac + + return 0 +} + +append_pcre_extend() { + echo "pcre_extend $1" >> "$CFGFILE" +} + +append_pcre() { + local match_type action regexp rewrite ace + + config_get match_type "$1" match_type + config_get action "$1" action + config_get regexp "$1" regexp + config_get rewrite "$1" rewrite + config_get ace "$1" ace + + [ -n "$match_type" ] && [ -n "$action" ] && [ -n "$regexp" ] || { + echo "3proxy: pcre '$1' needs match_type, action and regexp, ignored" >&2 + return 0 + } + + # Catch bad values here: 3proxy rejects the whole configuration on an + # unknown type or action, which would leave the router without a proxy. + case "$action" in + allow|deny|dunno) ;; + *) + echo "3proxy: pcre '$1' action '$action' is not allow, deny or dunno, ignored" >&2 + return 0 + ;; + esac + + local part + for part in $(echo "$match_type" | tr ',' ' '); do + case "$part" in + request|cliheader|srvheader|clidata|srvdata) ;; + *) + echo "3proxy: pcre '$1' match_type '$part' is unknown, ignored" >&2 + return 0 + ;; + esac + done + + if [ -n "$rewrite" ]; then + echo "pcre_rewrite $match_type $action $regexp $rewrite${ace:+ $ace}" >> "$CFGFILE" + else + echo "pcre $match_type $action $regexp${ace:+ $ace}" >> "$CFGFILE" + fi + + config_list_foreach "$1" extend append_pcre_extend + return 0 +} + +append_nsrecord() { + set -- $1 + if [ $# -ne 2 ]; then + echo "3proxy: nsrecord '$*' needs a hostname and an address, ignored" >&2 + return 0 + fi + + echo "nsrecord $1 $2" >> "$CFGFILE" + nsrecord_written=1 + return 0 +} + +append_nserver() { + echo "nserver $1" >> "$CFGFILE" +} + +append_user() { + users="$users $1" +} + +# $1 is the name of an acl section referenced by a service, or by the global +# section as the default access list. +append_acl() { + local action users src dst ports + + config_get action "$1" action allow + config_get users "$1" users + config_get src "$1" src + config_get dst "$1" dst + config_get ports "$1" ports + + case "$action" in + allow|deny) ;; + *) + echo "3proxy: acl '$1' has unknown action '$action', ignored" >&2 + return 0 + ;; + esac + + echo "$action ${users:-*} ${src:-*} ${dst:-*} ${ports:-*}" >> "$CFGFILE" + acl_written=1 + + if [ "$action" = "allow" ]; then + config_list_foreach "$1" parent append_parent + else + config_get _parent "$1" parent + [ -z "$_parent" ] || echo "3proxy: acl '$1' is a deny rule, its parents are ignored" >&2 + fi + + return 0 +} + +# $1 is the name of a parent section referenced by an acl. "parent" extends the +# allow rule that precedes it, so these are emitted directly after their rule. +append_parent() { + local weight type ip port username password line + + config_get weight "$1" weight 1000 + config_get type "$1" type + config_get ip "$1" ip + config_get port "$1" port + config_get username "$1" username + config_get password "$1" password + + [ -n "$type" ] && [ -n "$ip" ] && [ -n "$port" ] || { + echo "3proxy: parent '$1' needs type, ip and port, ignored" >&2 + return 0 + } + + line="parent $weight $type $ip $port" + if [ -n "$username" ]; then + line="$line $username" + [ -n "$password" ] && line="$line $password" + fi + + echo "$line" >> "$CFGFILE" + return 0 +} + +# TLS parameters that take a value. The UCI option name is the directive name. +SSL_VALUE_OPTIONS="ssl_server_cert ssl_server_key ssl_client_cert ssl_client_key + ssl_client_ciphersuites ssl_server_ciphersuites + ssl_client_cipher_list ssl_server_cipher_list + ssl_client_min_proto_version ssl_server_min_proto_version + ssl_client_max_proto_version ssl_server_max_proto_version + ssl_server_ca_file ssl_server_ca_key ssl_server_ca_dir ssl_server_ca_store + ssl_client_ca_file ssl_client_ca_dir ssl_client_ca_store + ssl_client_sni ssl_client_alpn ssl_client_mode ssl_certcache" + +# The TLS switches apply to every service below them, so they leak from one +# service to the next unless turned back off. These track what is currently in +# effect - all off, matching the defaults - so a directive is written only when +# a service actually needs a different state. +ssl_state_mitm=0 +ssl_state_server=0 +ssl_state_client=0 +ssl_state_client_verify=0 +ssl_state_server_verify=0 + +# $1 section, $2 uci option, $3 state variable, $4 directive on, $5 directive off +append_ssl_toggle() { + local want have + + config_get_bool want "$1" "$2" 0 + have=$(eval echo \$$3) + + [ "$want" = "$have" ] && return 0 + + if [ "$want" -gt 0 ]; then + echo "$4" >> "$CFGFILE" + else + echo "$5" >> "$CFGFILE" + fi + + eval "$3=$want" + return 0 +} + +append_ssl() { + local opt value mitm server cert key cverify + + for opt in $SSL_VALUE_OPTIONS; do + config_get value "$1" "$opt" + [ -n "$value" ] && echo "$opt $value" >> "$CFGFILE" + done + + append_ssl_toggle "$1" ssl_mitm ssl_state_mitm ssl_mitm ssl_nomitm + append_ssl_toggle "$1" ssl_server ssl_state_server ssl_serv ssl_noserv + append_ssl_toggle "$1" ssl_client ssl_state_client ssl_cli ssl_nocli + append_ssl_toggle "$1" ssl_client_verify ssl_state_client_verify \ + ssl_client_verify ssl_client_no_verify + append_ssl_toggle "$1" ssl_server_verify ssl_state_server_verify \ + ssl_server_verify ssl_server_no_verify + + config_get_bool mitm "$1" ssl_mitm 0 + config_get_bool cverify "$1" ssl_client_verify 0 + [ "$mitm" -gt 0 ] && [ "$cverify" -gt 0 ] || [ "$mitm" -eq 0 ] || \ + echo "3proxy: service '$1' spoofs certificates without ssl_client_verify, upstream certificates are not checked" >&2 + + config_get_bool server "$1" ssl_server 0 + if [ "$server" -gt 0 ]; then + config_get cert "$1" ssl_server_cert + config_get key "$1" ssl_server_key + [ -n "$cert" ] && [ -n "$key" ] || \ + echo "3proxy: service '$1' requires TLS from clients but has no ssl_server_cert/ssl_server_key" >&2 + fi + + return 0 +} + +append_service() { + local enabled type port bind external extra auth args + local bind_interface external_interface logformat + + config_get_bool enabled "$1" enabled 0 + [ "$enabled" -gt 0 ] || return 0 + + config_get type "$1" type + [ -n "$type" ] || { + echo "3proxy: service '$1' has no type, ignored" >&2 + return 0 + } + + config_get port "$1" port + config_get bind "$1" bind + config_get external "$1" external + config_get extra "$1" extra + config_get bind_interface "$1" bind_interface + config_get external_interface "$1" external_interface + config_get logformat "$1" logformat + config_get auth "$1" auth "$global_auth" + + echo "" >> "$CFGFILE" + echo "flush" >> "$CFGFILE" + [ -n "$auth" ] && echo "auth $auth" >> "$CFGFILE" + + # Rules referenced by the service, in the order they are listed. A service + # without its own list falls back to the global one. + acl_written=0 + config_list_foreach "$1" acl append_acl + [ "$acl_written" -gt 0 ] || config_list_foreach global acl append_acl + + [ -n "$logformat" ] && append_logformat "$logformat" + + append_ssl "$1" + + args="" + [ -n "$port" ] && args="$args -p$port" + [ -n "$bind" ] && args="$args -i$bind" + [ -n "$external" ] && args="$args -e$external" + [ -n "$bind_interface" ] && args="$args -Di$bind_interface" + [ -n "$external_interface" ] && args="$args -De$external_interface" + [ -n "$extra" ] && args="$args $extra" + + echo "$type$args" >> "$CFGFILE" + return 0 +} + +write_config() { + local nscache nscache6 maxconn log timeouts fakeresolve logformat + local authcache_type authcache_time authcache_size + local counter_file counter_type counter_name pcre_options + + mkdir -p "$(dirname "$CFGFILE")" + : > "$CFGFILE" + + config_get nscache global nscache + config_get nscache6 global nscache6 + config_get maxconn global maxconn + config_get global_auth global auth iponly + config_get log global log syslog + config_get timeouts global timeouts + config_get logformat global logformat + config_get_bool fakeresolve global fakeresolve 0 + config_get authcache_type global authcache_type + config_get authcache_time global authcache_time + config_get authcache_size global authcache_size + config_get counter_file global counter_file + config_get counter_type global counter_type + config_get counter_name global counter_name + config_get pcre_options global pcre_options + + config_list_foreach global nserver append_nserver + [ -n "$nscache" ] && echo "nscache $nscache" >> "$CFGFILE" + [ -n "$nscache6" ] && echo "nscache6 $nscache6" >> "$CFGFILE" + + # Static records are added to the cache, so they have to come after it. + nsrecord_written=0 + config_list_foreach global nsrecord append_nsrecord + [ "$nsrecord_written" -eq 0 ] || [ -n "$nscache$nscache6" ] || \ + echo "3proxy: nsrecord needs nscache or nscache6 to be set" >&2 + + case "$log" in + syslog) echo "log" >> "$CFGFILE" ;; + none|"") ;; + *) echo "log $log" >> "$CFGFILE" ;; + esac + + users="" + config_list_foreach global user append_user + [ -n "$users" ] && echo "users$users" >> "$CFGFILE" + + [ -n "$timeouts" ] && echo "timeouts $timeouts" >> "$CFGFILE" + [ "$fakeresolve" -gt 0 ] && echo "fakeresolve" >> "$CFGFILE" + [ -n "$logformat" ] && append_logformat "$logformat" + + if [ -n "$authcache_type" ]; then + [ -n "$authcache_time" ] || authcache_time=600 + echo "authcache $authcache_type $authcache_time${authcache_size:+ $authcache_size}" >> "$CFGFILE" + fi + + if [ -n "$counter_file" ]; then + echo "counter $counter_file${counter_type:+ $counter_type}${counter_name:+ $counter_name}" >> "$CFGFILE" + fi + + [ -n "$pcre_options" ] && echo "pcre_options $pcre_options" >> "$CFGFILE" + + # Both lists are order sensitive: 3proxy stops at the first match, so the + # exempting rules (nobandlimin and friends) have to be listed first. + config_list_foreach global pcre append_pcre + config_list_foreach global limit append_limit + + config_list_foreach global include append_include + config_list_foreach global extra_config append_line + + [ -n "$maxconn" ] && echo "maxconn $maxconn" >> "$CFGFILE" + + config_foreach append_service service + + return 0 +} + +start_service() { + local enabled + + config_load 3proxy + config_get_bool enabled global enabled 0 + + [ "$enabled" -gt 0 ] || { + echo "3proxy is disabled in /etc/config/3proxy" >&2 + return 1 + } + + write_config + + procd_open_instance + procd_set_param command "$PROG" "$CFGFILE" + procd_set_param file "$CFGFILE" + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "3proxy" +} + +reload_service() { + stop + start +}