From 1d4a964169c8298a4de962bee3f52a6930c8fb3a Mon Sep 17 00:00:00 2001 From: "Ycarus (Yannick Chabanois)" Date: Wed, 10 Jan 2024 17:38:33 +0100 Subject: [PATCH] Add nftables support to Shadowsocks-libev --- shadowsocks-libev/Makefile | 37 +- shadowsocks-libev/files/nft-rules/chain.uc | 122 ++++++ shadowsocks-libev/files/nft-rules/set.uc | 114 ++++++ shadowsocks-libev/files/nft-rules/ss-rules.uc | 8 + .../files/shadowsocks-libev.init-nft | 363 ++++++++++++++++++ 5 files changed, 640 insertions(+), 4 deletions(-) create mode 100644 shadowsocks-libev/files/nft-rules/chain.uc create mode 100644 shadowsocks-libev/files/nft-rules/set.uc create mode 100644 shadowsocks-libev/files/nft-rules/ss-rules.uc create mode 100755 shadowsocks-libev/files/shadowsocks-libev.init-nft diff --git a/shadowsocks-libev/Makefile b/shadowsocks-libev/Makefile index 6eea8b25c..b352558a7 100644 --- a/shadowsocks-libev/Makefile +++ b/shadowsocks-libev/Makefile @@ -44,8 +44,6 @@ endef define Package/shadowsocks-libev-config/install $(INSTALL_DIR) $(1)/etc/config $(INSTALL_DATA) ./files/shadowsocks-libev.config $(1)/etc/config/shadowsocks-libev - $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_BIN) ./files/shadowsocks-libev.init $(1)/etc/init.d/shadowsocks-libev endef @@ -92,8 +90,11 @@ define Package/shadowsocks-libev-ss-rules/install $(INSTALL_DATA) ./files/firewall.ss-rules $(1)/etc $(INSTALL_DIR) $(1)/etc/uci-defaults $(INSTALL_BIN) ./files/ss-rules.defaults $(1)/etc/uci-defaults - $(INSTALL_DIR) $(1)/etc/sysctl.d - $(INSTALL_DATA) ./files/shadowsocks.conf $(1)/etc/sysctl.d + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/shadowsocks-libev.init $(1)/etc/init.d/shadowsocks-libev + +# $(INSTALL_DIR) $(1)/etc/sysctl.d +# $(INSTALL_DATA) ./files/shadowsocks.conf $(1)/etc/sysctl.d # $(INSTALL_DIR) $(1)/usr/lib/shadowsocks-libev # $(INSTALL_DATA) $(PKG_BUILD_DIR)/src/*.ebpf $(1)/usr/lib/shadowsocks-libev endef @@ -108,6 +109,33 @@ uci batch <<-EOF EOF endef +define Package/shadowsocks-libev-ss-rules-nft + SECTION:=net + CATEGORY:=Network + SUBMENU:=Web Servers/Proxies + TITLE:=shadowsocks-libev ss-rules NFT + URL:=https://github.com/shadowsocks/shadowsocks-libev + DEPENDS:=+firewall4 \ + +ip \ + +resolveip \ + +ucode \ + +ucode-mod-fs \ + +shadowsocks-libev-ss-redir \ + +shadowsocks-libev-config \ + +kmod-nft-tproxy +endef + + +define Package/shadowsocks-libev-ss-rules-nft/install + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) ./files/ss-rules.defaults $(1)/etc/uci-defaults + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/shadowsocks-libev.init-nft $(1)/etc/init.d/shadowsocks-libev + $(INSTALL_DIR) $(1)/usr/share/ss-rules + $(INSTALL_DATA) ./files/nft-rules/* $(1)/usr/share/ss-rules/ +endef + + define Build/Prepare $(call Build/Prepare/Default) $(FIND) $(PKG_BUILD_DIR) \ @@ -128,6 +156,7 @@ TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include $(eval $(call BuildPackage,shadowsocks-libev-config)) $(eval $(call BuildPackage,shadowsocks-libev-ss-rules)) +$(eval $(call BuildPackage,shadowsocks-libev-ss-rules-nft)) $(foreach component,$(SHADOWSOCKS_COMPONENTS), \ $(eval $(call BuildPackage,shadowsocks-libev-$(component))) \ ) diff --git a/shadowsocks-libev/files/nft-rules/chain.uc b/shadowsocks-libev/files/nft-rules/chain.uc new file mode 100644 index 000000000..3047f1663 --- /dev/null +++ b/shadowsocks-libev/files/nft-rules/chain.uc @@ -0,0 +1,122 @@ +{% +function get_local_verdict() { + let v = o_local_default; + if (v == "checkdst") { + return "goto ss_rules_dst_" + proto; + } else if (v == "forward") { + return "goto ss_rules_forward_" + proto; + } else { + return null; + } +} + +function get_src_default_verdict() { + let v = o_src_default; + if (v == "checkdst") { + return "goto ss_rules_dst_" + proto; + } else if (v == "forward") { + return "goto ss_rules_forward_" + proto; + } else { + return "accept"; + } +} + +function get_dst_default_verdict() { + let v = o_dst_default; + if (v == "forward") { + return "goto ss_rules_forward_" + proto; + } else { + return "accept"; + } +} + +function get_ifnames() { + let res = []; + for (let ifname in split(o_ifnames, /[ \t\n]/)) { + ifname = trim(ifname); + if (ifname) push(res, ifname); + } + return res; +} + +let type, hook, priority, redir_port; +if (proto == "tcp") { + type = "nat"; + hook = "prerouting"; + priority = -1; + redir_port = o_redir_tcp_port; +} else if (proto == "udp") { + type = "filter"; + hook = "prerouting"; + priority = "mangle"; + redir_port = o_redir_udp_port; + if (system(" + set -o errexit + iprr() { + while ip $1 rule del fwmark 1 lookup 100 2>/dev/null; do true; done + ip $1 rule add fwmark 1 lookup 100 + ip $1 route flush table 100 2>/dev/null || true + ip $1 route add local default dev lo table 100 + } + iprr -4 + iprr -6 + ") != 0) { + return ; + } +} else { + return; +} + +%} +{% if (redir_port): %} + +chain ss_rules_pre_{{ proto }} { + type {{ type }} hook {{ hook }} priority {{ priority }}; + meta l4proto {{ proto }}{%- let ifnames=get_ifnames(); if (length(ifnames)): %} iifname { {{join(", ", ifnames)}} }{% endif %} goto ss_rules_pre_src_{{ proto }}; +} + +chain ss_rules_pre_src_{{ proto }} { + ip daddr @ss_rules_dst_bypass_ accept; + ip6 daddr @ss_rules6_dst_bypass_ accept; + goto ss_rules_src_{{ proto }}; +} + +chain ss_rules_src_{{ proto }} { + ip saddr @ss_rules_src_bypass accept; + ip saddr @ss_rules_src_forward goto ss_rules_forward_{{ proto }}; + ip saddr @ss_rules_src_checkdst goto ss_rules_dst_{{ proto }}; + ip6 saddr @ss_rules6_src_bypass accept; + ip6 saddr @ss_rules6_src_forward goto ss_rules_forward_{{ proto }}; + ip6 saddr @ss_rules6_src_checkdst goto ss_rules_dst_{{ proto }}; + {{ get_src_default_verdict() }}; +} + +chain ss_rules_dst_{{ proto }} { + ip daddr @ss_rules_dst_bypass accept; + ip daddr @ss_rules_dst_forward goto ss_rules_forward_{{ proto }}; + ip6 daddr @ss_rules6_dst_bypass accept; + ip6 daddr @ss_rules6_dst_forward goto ss_rules_forward_{{ proto }}; + {{ get_dst_default_verdict() }}; +} + +{% if (proto == "tcp"): %} +chain ss_rules_forward_{{ proto }} { + meta l4proto tcp {{ o_nft_tcp_extra }} redirect to :{{ redir_port }}; +} +{% let local_verdict = get_local_verdict(); if (local_verdict): %} +chain ss_rules_local_out { + type {{ type }} hook output priority -1; + meta l4proto != tcp accept; + ip daddr @ss_rules_dst_bypass_ accept; + ip daddr @ss_rules_dst_bypass accept; + ip6 daddr @ss_rules6_dst_bypass_ accept; + ip6 daddr @ss_rules6_dst_bypass accept; + {{ local_verdict }}; +} +{% endif %} +{% elif (proto == "udp"): %} +chain ss_rules_forward_{{ proto }} { + meta l4proto udp {{ o_nft_udp_extra }} meta mark set 1 tproxy to :{{ redir_port }}; +} +{% endif %} +{% endif %} diff --git a/shadowsocks-libev/files/nft-rules/set.uc b/shadowsocks-libev/files/nft-rules/set.uc new file mode 100644 index 000000000..38140e7e1 --- /dev/null +++ b/shadowsocks-libev/files/nft-rules/set.uc @@ -0,0 +1,114 @@ +{% +let fs = require("fs"); + +let o_dst_bypass4_ = " + 0.0.0.0/8 + 10.0.0.0/8 + 100.64.0.0/10 + 127.0.0.0/8 + 169.254.0.0/16 + 172.16.0.0/12 + 192.0.0.0/24 + 192.0.2.0/24 + 192.31.196.0/24 + 192.52.193.0/24 + 192.88.99.0/24 + 192.168.0.0/16 + 192.175.48.0/24 + 198.18.0.0/15 + 198.51.100.0/24 + 203.0.113.0/24 + 224.0.0.0/4 + 240.0.0.0/4 +"; +let o_dst_bypass6_ = " + ::1/128 + ::/128 + ::ffff:0:0/96 + 64:ff9b:1::/48 + 100::/64 + fe80::/10 + 2001::/23 + fc00::/7 +"; +let o_dst_bypass_ = o_dst_bypass4_ + " " + o_dst_bypass6_; + +let set_suffix = { + "src_bypass": { + str: o_src_bypass, + }, + "src_forward": { + str: o_src_forward, + }, + "src_checkdst": { + str: o_src_checkdst, + }, + "dst_bypass": { + str: o_dst_bypass, + file: o_dst_bypass_file, + }, + "dst_bypass_": { + str: o_dst_bypass_, + }, + "dst_forward": { + str: o_dst_forward, + file: o_dst_forward_file, + }, + "dst_forward_rrst_": {}, +}; + +function set_name(suf, af) { + if (af == 4) { + return "ss_rules_"+suf; + } else { + return "ss_rules6_"+suf; + } +} + +function set_elements_parse(res, str, af) { + for (let addr in split(str, /[ \t\n]/)) { + addr = trim(addr); + if (!addr) continue; + if (af == 4 && index(addr, ":") != -1) continue; + if (af == 6 && index(addr, ":") == -1) continue; + push(res, addr); + } +} + +function set_elements(suf, af) { + let obj = set_suffix[suf]; + let res = []; + let addr; + + let str = obj["str"]; + if (str) { + set_elements_parse(res, str, af); + } + + let file = obj["file"]; + if (file) { + let fd = fs.open(file); + if (fd) { + str = fd.read("all"); + set_elements_parse(res, str, af); + } + } + + return res; +} +%} + +{% for (let suf in set_suffix): for (let af in [4, 6]): %} +set {{ set_name(suf, af) }} { + type ipv{{af}}_addr; + flags interval; + auto-merge; +{% let elems = set_elements(suf, af); if (length(elems)): %} + elements = { +{% for (let i = 0; i < length(elems); i++): %} + {{ elems[i] }}{% if (i < length(elems) - 1): %},{% endif %}{% print("\n") %} +{% endfor %} + } +{% endif %} +} +{% endfor; endfor %} diff --git a/shadowsocks-libev/files/nft-rules/ss-rules.uc b/shadowsocks-libev/files/nft-rules/ss-rules.uc new file mode 100644 index 000000000..f3955b2ef --- /dev/null +++ b/shadowsocks-libev/files/nft-rules/ss-rules.uc @@ -0,0 +1,8 @@ +{% + +include("set.uc"); +include("chain.uc", {proto: "tcp"}); +include("chain.uc", {proto: "udp"}); + +%} + diff --git a/shadowsocks-libev/files/shadowsocks-libev.init-nft b/shadowsocks-libev/files/shadowsocks-libev.init-nft new file mode 100755 index 000000000..3db4bbc56 --- /dev/null +++ b/shadowsocks-libev/files/shadowsocks-libev.init-nft @@ -0,0 +1,363 @@ +#!/bin/sh /etc/rc.common +# +# Copyright (C) 2017-2019 Yousong Zhou +# +# This is free software, licensed under the GNU General Public License v3. +# See /LICENSE for more information. +# + +USE_PROCD=1 +START=99 + +EXTRA_COMMANDS="rules_up rules_down rules_exist" + +ss_confdir=/var/etc/shadowsocks-libev +ss_bindir=/usr/bin + +ssrules_uc="/usr/share/ss-rules/ss-rules.uc" +ssrules_nft="/etc/nftables.d/90-ss-rules.nft" + +ss_mkjson_server_conf() { + local cfgserver + + config_get cfgserver "$cfg" server + [ -n "$cfgserver" ] || return 1 + eval "$(validate_server_section "$cfg" ss_validate_mklocal)" + validate_server_section "$cfgserver" || return 1 + [ "$disabled" = 0 ] || return 1 + ss_mkjson_server_conf_ "$cfgserver" +} + +ss_mkjson_server_conf_() { + [ -n "$server_port" ] || return 1 + [ -z "$server" ] || json_add_string server "$server" + json_add_int server_port "$server_port" + [ -z "$method" ] || json_add_string method "$method" + [ -z "$key" ] || json_add_string key "$key" + [ -z "$password" ] || json_add_string password "$password" + [ -z "$plugin" ] || json_add_string plugin "$plugin" + [ -z "$plugin_opts" ] || json_add_string plugin_opts "$plugin_opts" +} + +ss_mkjson_ss_local_conf() { + ss_mkjson_server_conf +} + +ss_mkjson_ss_redir_conf() { + ss_mkjson_server_conf +} + +ss_mkjson_ss_server_conf() { + ss_mkjson_server_conf_ +} + +ss_mkjson_ss_tunnel_conf() { + ss_mkjson_server_conf || return 1 + [ -n "$tunnel_address" ] || return 1 + json_add_string tunnel_address "$tunnel_address" +} + +ss_xxx() { + local cfg="$1" + local cfgtype="$2" + local bin="$ss_bindir/${cfgtype/_/-}" + local confjson="$ss_confdir/$cfgtype.$cfg.json" + + [ -x "$bin" ] || return + eval "$("validate_${cfgtype}_section" "$cfg" ss_validate_mklocal)" + "validate_${cfgtype}_section" "$cfg" || return + [ "$disabled" = 0 ] || return + + json_init + ss_mkjson_${cfgtype}_conf || return + json_add_boolean use_syslog 1 + json_add_boolean ipv6_first "$ipv6_first" + json_add_boolean fast_open "$fast_open" + json_add_boolean reuse_port "$reuse_port" + json_add_boolean no_delay "$no_delay" + json_add_boolean mptcp "$mptcp" + [ -z "$local_address" ] || json_add_string local_address "$local_address" + [ -z "$local_port" ] || json_add_int local_port "$local_port" + [ -z "$local_ipv4_address" ] || json_add_string local_ipv4_address "$local_ipv4_address" + [ -z "$local_ipv6_address" ] || json_add_string local_ipv6_address "$local_ipv6_address" + [ -z "$mode" ] || json_add_string mode "$mode" + [ -z "$mtu" ] || json_add_int mtu "$mtu" + [ -z "$timeout" ] || json_add_int timeout "$timeout" + [ -z "$user" ] || json_add_string user "$user" + [ -z "$acl" ] || json_add_string acl "$acl" + json_dump -i >"$confjson" + + procd_open_instance "$cfgtype.$cfg" + procd_set_param command "$bin" -c "$confjson" + [ "$verbose" = 0 ] || procd_append_param command -v + if [ -n "$bind_address" ]; then + echo "$cfgtype $cfg: uci option bind_address deprecated, please switch to local_address" >&2 + procd_append_param command -b "$bind_address" + fi + procd_set_param file "$confjson" + procd_set_param respawn + procd_close_instance + ss_rules_cb +} + +ss_rules_cb() { + local cfgserver server + + if [ "$cfgtype" = ss_redir ]; then + config_get cfgserver "$cfg" server + config_get server "$cfgserver" server + ss_redir_servers="$ss_redir_servers $server" + if [ "$mode" = tcp_only -o "$mode" = "tcp_and_udp" ]; then + eval "ss_rules_redir_tcp_$cfg=$local_port" + fi + if [ "$mode" = udp_only -o "$mode" = "tcp_and_udp" ]; then + eval "ss_rules_redir_udp_$cfg=$local_port" + fi + fi +} + +ss_rules_nft_gen() { + local cfg="ss_rules" + local cfgtype + local local_port_tcp local_port_udp + local remote_servers + + [ -s "$ssrules_uc" ] || return 1 + + config_get cfgtype "$cfg" TYPE + [ "$cfgtype" = ss_rules ] || return 1 + + eval "$(validate_ss_rules_section "$cfg" ss_validate_mklocal)" + validate_ss_rules_section "$cfg" || return 1 + [ "$disabled" = 0 ] || return 2 + + if [ "$ss_rules_redir_tcp_$redir_tcp" = "all" ]; then + min_ss_redir_ports="65535" + max_ss_redir_ports="0" + config_load shadowsocks-libev + config_foreach ss_redir_ports ss_redir $cfgrulesserver + if [ "$min_ss_redir_ports" != "$max_ss_redir_ports" ]; then + all_ss_redir_ports=$min_ss_redir_ports-$max_ss_redir_ports + else + all_ss_redir_ports=$min_ss_redir_ports + fi + local_port_tcp="$all_ss_redir_ports" + if [ "$ss_rules_redir_udp_$redir_udp" = "all" ] || [ "$ss_rules_redir_udp_$redir_udp" = "hi1" ]; then + local_port_udp="$min_ss_redir_ports" + fi + else + eval local_port_tcp="\$ss_rules_redir_tcp_$redir_tcp" + eval local_port_udp="\$ss_rules_redir_udp_$redir_udp" + fi + [ -n "$local_port_tcp" -o -n "$local_port_udp" ] || return 1 + remote_servers="$(echo $ss_redir_servers \ + | tr ' ' '\n' \ + | sort -u \ + | xargs -n 1 resolveip \ + | sort -u)" + + local tmp="/tmp/ssrules" + json_init + json_add_string o_remote_servers "$remote_servers" + json_add_int o_redir_tcp_port "$local_port_tcp" + json_add_int o_redir_udp_port "$local_port_udp" + json_add_string o_ifnames "$ifnames" + json_add_string o_local_default "$local_default" + json_add_string o_src_bypass "$src_ips_bypass" + json_add_string o_src_forward "$src_ips_forward" + json_add_string o_src_checkdst "$src_ips_checkdst" + json_add_string o_src_default "$src_default" + json_add_string o_dst_bypass "$dst_ips_bypass" + json_add_string o_dst_forward "$dst_ips_forward" + json_add_string o_dst_bypass_file "$dst_ips_bypass_file" + json_add_string o_dst_forward_file "$dst_ips_forward_file" + json_add_string o_dst_default "$dst_default" + json_add_string o_nft_tcp_extra "$nft_tcp_extra" + json_add_string o_nft_udp_extra "$nft_udp_extra" + json_dump -i >"$tmp.json" + + if utpl -S -F "$tmp.json" "$ssrules_uc" >"$tmp.nft" \ + && ! cmp -s "$tmp.nft" "$ssrules_nft"; then + echo "table inet chk {include \"$tmp.nft\";}" >"$tmp.nft.chk" + if nft -f "$tmp.nft.chk" -c; then + mv "$tmp.nft" "$ssrules_nft" + fw4 restart + fi + rm -f "$tmp.nft.chk" + fi + rm -f "$tmp.json" + rm -f "$tmp.nft" +} + +ss_rules_nft_reset() { + if [ -f "$ssrules_nft" ]; then + rm -f "$ssrules_nft" + fw4 restart + fi +} + +ss_rules() { + if ! ss_rules_nft_gen; then + ss_rules_nft_reset + fi +} + +rules_up() { + if [ -f "${ssrules_nft}.down" ]; then + mv -f "${ssrules_nft}.down" "$ssrules_nft" + fw4 restart + fi +} + +rules_down() { + if [ -f "${ssrules_nft}" ]; then + mv -f "$ssrules_nft" "${ssrules_nft}.down" + fw4 restart + fi +} + +rules_exist() { + if [ -f "$ssrules_nft" ]; then + return 0 + else + return 1 + fi +} + +start_service() { + local cfgtype + + mkdir -p "$ss_confdir" + config_load shadowsocks-libev + for cfgtype in ss_local ss_redir ss_server ss_tunnel; do + config_foreach ss_xxx "$cfgtype" "$cfgtype" + done + ss_rules +} + +stop_service() { + ss_rules_nft_reset + rm -rf "$ss_confdir" +} + +service_triggers() { + procd_add_reload_interface_trigger wan + procd_add_reload_trigger shadowsocks-libev + procd_open_validate + validate_server_section + validate_ss_local_section + validate_ss_redir_section + validate_ss_rules_section + validate_ss_server_section + validate_ss_tunnel_section + procd_close_validate +} + +ss_validate_mklocal() { + local tuple opts + + shift 2 + for tuple in "$@"; do + opts="${tuple%%:*} $opts" + done + [ -z "$opts" ] || echo "local $opts" +} + +ss_validate() { + uci_validate_section shadowsocks-libev "$@" +} + +validate_common_server_options_() { + local cfgtype="$1"; shift + local cfg="$1"; shift + local func="$1"; shift + local stream_methods='"none", "plain", "chacha20-ietf-poly1305"' + local aead_methods='"aes-128-gcm", "aes-256-gcm","2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "chacha20-ietf-poly1305", "2022-blake3-chacha20-poly1305","2022-blake3-chacha8-poly1305"' + + + "${func:-ss_validate}" "$cfgtype" "$cfg" "$@" \ + 'disabled:bool:0' \ + 'server:host' \ + 'server_port:port' \ + 'password:string' \ + 'key:string' \ + "method:or($stream_methods, $aead_methods)" \ + 'plugin:string' \ + 'plugin_opts:string' +} + +validate_common_client_options_() { + validate_common_options_ "$@" \ + 'server:uci("shadowsocks-libev", "@server")' \ + 'local_address:ipaddr:0.0.0.0' \ + 'local_port:port' +} + +validate_common_options_() { + local cfgtype="$1"; shift + local cfg="$1"; shift + local func="$1"; shift + + "${func:-ss_validate}" "$cfgtype" "$cfg" "$@" \ + 'disabled:bool:0' \ + 'fast_open:bool:0' \ + 'ipv6_first:bool:0' \ + 'no_delay:bool:0' \ + 'reuse_port:bool:0' \ + 'mptcp:bool:0' \ + 'verbose:bool:0' \ + 'mode:or("tcp_only", "udp_only", "tcp_and_udp"):tcp_only' \ + 'mtu:uinteger' \ + 'timeout:uinteger' \ + 'user:string' +} + +validate_server_section() { + validate_common_server_options_ server "$1" "$2" +} + +validate_ss_local_section() { + validate_common_client_options_ ss_local "$1" "$2" \ + 'acl:file' +} + +validate_ss_redir_section() { + validate_common_client_options_ ss_redir "$1" "$2" +} + +validate_ss_rules_section() { + "${2:-ss_validate}" ss_rules "$1" \ + 'disabled:bool:0' \ + 'redir_tcp:uci("shadowsocks-libev", "@ss_redir","all")' \ + 'redir_udp:uci("shadowsocks-libev", "@ss_redir","all")' \ + 'src_ips_bypass:or(ipaddr,cidr)' \ + 'src_ips_forward:or(ipaddr,cidr)' \ + 'src_ips_checkdst:or(ipaddr,cidr)' \ + 'dst_ips_bypass_file:file' \ + 'dst_ips_bypass:or(ipaddr,cidr)' \ + 'dst_ips_forward_file:file' \ + 'dst_ips_forward:or(ipaddr,cidr)' \ + 'src_default:or("bypass", "forward", "checkdst"):checkdst' \ + 'dst_default:or("bypass", "forward"):bypass' \ + 'local_default:or("bypass", "forward", "checkdst"):bypass' \ + 'nft_tcp_extra:string' \ + 'nft_udp_extra:string' \ + 'ifnames:maxlength(15)' +} + +validate_ss_server_section() { + validate_common_server_options_ ss_server "$1" \ + validate_common_options_ \ + "$2" \ + 'local_address:ipaddr' \ + 'local_ipv4_address:ip4addr' \ + 'local_ipv6_address:ip6addr' \ + 'bind_address:ipaddr' \ + 'acl:file' +} + +validate_ss_tunnel_section() { + validate_common_client_options_ ss_tunnel "$1" \ + "$2" \ + 'tunnel_address:regex(".+\:[0-9]+")' +}