3proxy/scripts/openwrt/files/3proxy.init
Vladimir Dubrovin 2cbdc6e845 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.
2026-08-23 15:40:26 +03:00

461 lines
12 KiB
Bash

#!/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
}