#!/usr/bin/utpl -S {# Thanks to homeproxy -#} {%- import { readfile } from 'fs'; import { cursor } from 'uci'; import { isEmpty, yqRead } from '/etc/fchomo/scripts/fchomo.uc'; const fw4 = require('fw4'); function array_to_nftarr(array) { if (type(array) !== 'array') return null; return `{ ${join(', ', uniq(array))} }`; } function resolve_ipv6(str) { if (isEmpty(str)) return null; let ipv6 = fw4.parse_subnet(str)?.[0]; if (!ipv6 || ipv6.family !== 6) return null; if (ipv6.bits > -1) return `${ipv6.addr}/${ipv6.bits}`; else return `& ${ipv6.mask} == ${ipv6.addr}`; } function resolve_mark(str) { if (isEmpty(str)) return null; let mark = fw4.parse_mark(str); if (isEmpty(mark)) return null; if (mark.mask === 0xffffffff) return fw4.hex(mark.mark); else if (mark.mark === 0) return `mark and ${fw4.hex(~mark.mask & 0xffffffff)}`; else if (mark.mark === mark.mask) return `mark or ${fw4.hex(mark.mark)}`; else if (mark.mask === 0) return `mark xor ${fw4.hex(mark.mark)}`; else return `mark and ${fw4.hex(~mark.mask & 0xffffffff)} xor ${fw4.hex(mark.mark)}`; } /* Misc config */ const resources_dir = '/etc/fchomo/resources'; /* UCI config start */ const cfgname = 'fchomo'; const uci = cursor(); uci.load(cfgname); const common_tcpport = uci.get(cfgname, 'config', 'common_tcpport') || '20-21,22,53,80,110,143,443,465,853,873,993,995,8080,8443,9418', common_udpport = uci.get(cfgname, 'config', 'common_udpport') || '20-21,22,53,80,110,143,443,853,993,995,8080,8443,9418', stun_port = uci.get(cfgname, 'config', 'stun_port') || '3478,19302', tun_name = uci.get(cfgname, 'config', 'tun_name') || 'hmtun0', self_mark = uci.get(cfgname, 'config', 'self_mark') || '200', tproxy_mark = resolve_mark(uci.get(cfgname, 'config', 'tproxy_mark') || '201'), tun_mark = resolve_mark(uci.get(cfgname, 'config', 'tun_mark') || '202'); const redir_port = uci.get(cfgname, 'inbound', 'redir_port') || '7891', tproxy_port = uci.get(cfgname, 'inbound', 'tproxy_port') || '7892', tunnel_port = uci.get(cfgname, 'inbound', 'tunnel_port') || '7893', proxy_mode = uci.get(cfgname, 'inbound', 'proxy_mode') || 'redir_tproxy'; const global_ipv6 = uci.get(cfgname, 'global', 'ipv6') || '1', dns_ipv6 = uci.get(cfgname, 'dns', 'ipv6') || '1', dns_port = uci.get(cfgname, 'dns', 'dns_port') || '7853'; const dnsmasq_hijacked = uci.get('dhcp', '@dnsmasq[0]', 'dns_redirect') || '0', dnsmasq_port = uci.get('dhcp', '@dnsmasq[0]', 'port') || '53'; let client_enabled, routing_tcpport, routing_udpport, routing_mode, routing_domain; client_enabled = uci.get(cfgname, 'routing', 'client_enabled') || '0', routing_tcpport = uci.get(cfgname, 'routing', 'routing_tcpport') || null; routing_udpport = uci.get(cfgname, 'routing', 'routing_udpport') || null; routing_mode = uci.get(cfgname, 'routing', 'routing_mode') || null; routing_domain = uci.get(cfgname, 'routing', 'routing_domain') || '0'; if (routing_tcpport === 'common') routing_tcpport = common_tcpport; else if (routing_tcpport === 'common_stun') routing_tcpport = `${common_tcpport},${stun_port}`; if (routing_udpport === 'common') routing_udpport = common_udpport; else if (routing_udpport === 'common_stun') routing_udpport = `${common_udpport},${stun_port}`; if (!routing_mode) routing_domain = '0'; const proxy_router = uci.get(cfgname, 'routing', 'proxy_router') || '1'; const control_options = [ "listen_interfaces", "lan_filter", "lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips", "lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips" ]; const control_info = {}; for (let i in control_options) control_info[i] = uci.get(cfgname, 'routing', i); control_info.wan_direct_ipv4_ips = json(trim(yqRead('-oj', '.IPCIDR', resources_dir + '/direct_list.yaml')) || '[]'); control_info.wan_direct_ipv6_ips = json(trim(yqRead('-oj', '.IPCIDR6', resources_dir + '/direct_list.yaml')) || '[]'); control_info.wan_proxy_ipv4_ips = json(trim(yqRead('-oj', '.IPCIDR', resources_dir + '/proxy_list.yaml')) || '[]'); control_info.wan_proxy_ipv6_ips = json(trim(yqRead('-oj', '.IPCIDR6', resources_dir + '/proxy_list.yaml')) || '[]'); /* UCI config end */ -%} {# Common function START #} {%- function render_acl_src(inchain, outchain): %} chain {{ inchain }} { {% if (control_info.listen_interfaces): %} meta iifname != {{ array_to_nftarr(split(join(' ', control_info.listen_interfaces) + ' lo', ' ')) }} counter return {% endif %} meta mark {{ self_mark }} counter return {% if (control_info.lan_filter === 'white_list'): %} {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto {{ outchain }} {% endif %} {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto {{ outchain }} {% endfor %} {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto {{ outchain }} {% endif %} {% elif (control_info.lan_filter === 'black_list'): %} {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return {% endif %} {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} ip6 saddr {{ resolve_ipv6(ipv6) }} counter return {% endfor %} {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return {% endif %} {% endif /* lan_filter */ %} {% if (control_info.lan_filter !== 'white_list'): %} counter goto {{ outchain }} {% endif %} } {% endfunction %} {%- function render_acl_dst(inchain, outchain): %} chain {{ inchain }} { meta mark {{ self_mark }} counter return fib daddr type { local } counter return ct direction reply counter return ip daddr @inet4_local_addr counter return {% if (global_ipv6 === '1'): %} ip6 daddr @inet6_local_addr counter return {% endif %} ip daddr @inet4_wan_proxy_addr counter goto {{ outchain }} {% if (global_ipv6 === '1'): %} ip6 daddr @inet6_wan_proxy_addr counter goto {{ outchain }} {% endif %} ip daddr @inet4_wan_direct_addr counter return {% if (global_ipv6 === '1'): %} ip6 daddr @inet6_wan_direct_addr counter return {% endif %} {% if (routing_mode === 'routing_gfw'): %} ip daddr != @inet4_gfw_list_addr counter return {% if (global_ipv6 === '1'): %} ip6 daddr != @inet6_gfw_list_addr counter return {% endif %} {% elif (routing_mode === 'bypass_cn'): %} ip daddr @inet4_china_list_addr counter return {% if (global_ipv6 === '1'): %} ip6 daddr @inet6_china_list_addr counter return {% endif %} {% endif /* routing_mode */ %} counter goto {{ outchain }} } {% endfunction %} {%- function render_acl_dport(inchain, outchain, l4proto): %} chain {{ inchain }} { {#- DNS hijack #} meta l4proto { tcp, udp } th dport 53 counter goto {{ outchain }} comment "!{{ cfgname }}: DNS hijack" {% if ((l4proto === 'tcp' || !l4proto) && routing_tcpport): %} tcp dport != @tcp_routing_port counter return {% endif %} {% if ((l4proto === 'udp' || !l4proto) && routing_udpport): %} udp dport != @udp_routing_port counter return {% endif %} counter goto {{ outchain }} } {% endfunction %} {# Common function END -#} table inet fchomo { {#- Reserved addresses #} set inet4_local_addr { type ipv4_addr flags interval auto-merge elements = { 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, {# 172.25.26.0/30, https://github.com/muink/openwrt-alwaysonline.git -#} 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, 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, 255.255.255.255/32 } } {% if (global_ipv6 === '1'): %} set inet6_local_addr { type ipv6_addr flags interval auto-merge elements = { ::/128, ::1/128, ::ffff:0:0/96, ::ffff:0:0:0/96, 64:ff9b::/96, 64:ff9b:1::/48, 100::/64, 2001::/32, 2001:10::/28, 2001:20::/28, 2001:db8::/32, 2002::/16, 3fff::/20, 5f00::/16, fc00::/7, {# fdfe:aead:2526::0/126, https://github.com/muink/openwrt-alwaysonline.git -#} fe80::/10, ff00::/8 } } {% endif %} {#- Custom Direct list #} set inet4_wan_direct_addr { type ipv4_addr flags interval auto-merge elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} } } {% if (global_ipv6 === '1'): %} set inet6_wan_direct_addr { type ipv6_addr flags interval auto-merge elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} } } {% endif %} {#- Custom Proxy list #} set inet4_wan_proxy_addr { type ipv4_addr flags interval auto-merge elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} } } {% if (global_ipv6 === '1'): %} set inet6_wan_proxy_addr { type ipv6_addr flags interval auto-merge elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} } } {% endif %} {#- Routing mode #} {% if (match(routing_mode, /bypass_cn/)): %} set inet4_china_list_addr { type ipv4_addr flags interval auto-merge elements = { {{ join(', ', split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)) }} } } {% if (global_ipv6 === '1'): %} set inet6_china_list_addr { type ipv6_addr flags interval auto-merge elements = { {{ join(', ', split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)) }} } } {% endif %} {% elif (match(routing_mode, /routing_gfw/)): %} set inet4_gfw_list_addr { type ipv4_addr flags interval auto-merge elements = {} } {% if (global_ipv6 === '1'): %} set inet6_gfw_list_addr { type ipv6_addr flags interval auto-merge elements = {} } {% endif %} {% endif /* routing_mode */ %} {#- Routing port #} {% if (routing_tcpport): %} set tcp_routing_port { type inet_service flags interval auto-merge elements = { {{ join(', ', split(routing_tcpport, ',')) }} } } {% endif %} {% if (routing_udpport): %} set udp_routing_port { type inet_service flags interval auto-merge elements = { {{ join(', ', split(routing_udpport, ',')) }} } } {% endif %} {# Main entrypoint START #} {# https://en.wikipedia.org/wiki/Netfilter#/media/File:Netfilter-packet-flow.svg #} chain dstnat { type nat hook prerouting priority dstnat + 5; policy accept; {#- DNS hijack #} {% if (dnsmasq_hijacked !== '1'): %} {% if (control_info.listen_interfaces): %} meta iifname {{ array_to_nftarr(control_info.listen_interfaces) }} {% endif %} meta iifname != lo meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { tcp, udp } th dport 53 counter redirect to :{{ dnsmasq_port }} comment "!{{ cfgname }}: DNS hijack (subnet)" {% endif /* dnsmasq_hijacked */ %} {#- TCP redirect entrypoint #} {% if (match(proxy_mode, /redir/)): %} meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump redir_acl_src {% endif %} } chain prerouting { type filter hook prerouting priority 5; policy accept; {#- DNS hijack #} fib daddr type local meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { tcp, udp } th dport { {{ join(', ', [dnsmasq_port, dns_port, tunnel_port]) }} } counter accept comment "!{{ cfgname }}: DNS hijack (bypass local dnsserver)" {#- UDP tproxy entrypoint #} {% if (match(proxy_mode, /tproxy/)): %} meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump tproxy_acl_src {#- TUN entrypoint #} {% elif (match(proxy_mode, /tun/)): %} iifname {{ tun_name }} counter accept meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump tun_acl_src {% endif %} } {% if (proxy_router === '1'): %} chain mangle_output { type route hook output priority 0; policy accept; {#- UDP tproxy entrypoint #} {% if (match(proxy_mode, /tproxy/)): %} meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump tproxy_acl_dst_reroute {#- TUN entrypoint #} {% elif (match(proxy_mode, /tun/)): %} iifname {{ tun_name }} counter accept meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump tun_acl_dst {% endif %} } {#- TCP redirect entrypoint #} {% if (match(proxy_mode, /redir/)): %} chain nat_output { type nat hook output priority 0; policy accept; meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump redir_acl_dst } {% endif %} {% endif /* proxy_router */ %} {# Main entrypoint END #} {# TCP redirect START #} {% if (match(proxy_mode, /redir/)): %} {{ render_acl_src('redir_acl_src', 'redir_acl_dst') }} {{ render_acl_dst('redir_acl_dst', 'redir_acl_dport') }} {{ render_acl_dport('redir_acl_dport', 'redir_gate', 'tcp') }} chain redir_gate { {#- DNS hijack #} tcp dport 53 counter redirect to :{{ tunnel_port }} comment "!{{ cfgname }}: DNS hijack (router tcp)" meta l4proto tcp counter redirect to :{{ redir_port }} } {% endif /* proxy_mode */ %} {# TCP redirect END #} {# UDP tproxy START #} {% if (match(proxy_mode, /tproxy/)): %} {{ render_acl_src('tproxy_acl_src', 'tproxy_acl_dst') }} {{ render_acl_dst('tproxy_acl_dst', 'tproxy_acl_dport') }} {{ render_acl_dport('tproxy_acl_dport', 'tproxy_gate', 'udp') }} chain tproxy_gate { meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to :{{ tproxy_port }} counter accept {% if (global_ipv6 === '1'): %} meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to :{{ tproxy_port }} counter accept {% endif %} } {% if (proxy_router === '1'): %} {{ render_acl_dst('tproxy_acl_dst_reroute', 'tproxy_acl_dport_reroute') }} {{ render_acl_dport('tproxy_acl_dport_reroute', 'tproxy_mark', 'udp') }} chain tproxy_mark { {#- DNS hijack (router udp) #} {# tproxy_mark --> route_table_id --reroute-to--> lo --> prerouting #} meta l4proto udp meta mark set {{ tproxy_mark }} counter accept } {% endif /* proxy_router */ %} {% endif /* proxy_mode */ %} {# UDP tproxy END #} {# TUN START #} {% if (match(proxy_mode, /tun/)): %} {{ render_acl_src('tun_acl_src', 'tun_acl_dst') }} {{ render_acl_dst('tun_acl_dst', 'tun_acl_dport') }} {{ render_acl_dport('tun_acl_dport', 'tun_mark', (proxy_mode === 'tun') ? '' : 'udp') }} chain tun_mark { meta mark set {{ tun_mark }} counter accept } {% endif /* proxy_mode */ %} {# TUN END #} }