家庭网关全新升级

易雾君
发布于 2024-01-21 / 55 阅读
2
0

家庭网关全新升级

前言

家庭网关在之前很长一段时间,选用的 OpenWrt 21.02.1 ,默认使用的 iptables 做的策略选路,而目前的新版本基本都是已经弃用的iptables,而是逐渐采用 nftables ,不过最近终究是完成了迁移,让线路管理更加的规范化。本次升级网关仅限家庭数据中心,不包含广州腾讯云服务器。

需求闲谈

我的家庭网关重点在于策略线路,其他都不是重点,原则上按照白名单模式,最小化按需外联,假定本次要满足如下 5 项需求。

  1. 禁止联网:默认不让家庭数据中心的所有机器外联互联网;
  2. 家宽直连:仅限指定机器进行外联网络,由家庭宽带直接出去;
  3. 安全测试拦截代理审查:将网络中的手机流量导入拦截代理工具burpsuite,开展安全测试;
  4. 域名白名单限定:仅允许指定机器访问指定域名清单;
  5. 策略线路需区分固定和临时:有些资产需长期满足走某线路,而有些资产仅需要临时放行,如升级系统期间。

设计思想

先谈谈网络架构,家庭网关前置有个网络运营商的光猫,后端部署有一台防火墙,防火墙后端就是接入家庭数据中心的核心机器,防火墙配合私有云管平台实现分区分域,外加开启流量侧主动防御,识别网络威胁流量并及时阻断。而网关重点管控网络流量的出口,仅限经过授权的机器访问授权的互联网资产,有效降低机器失陷后反弹成功率。

言归正传,总结如上五条需求,就是需要实现基于策略选路,设计优先保障稳定性,像家庭宽带直连,可以通过伪装实现,而安全测试的拦截代理仅支持tcp协议,可采取tproxy来达成导流,域名白名单则需要配合dnsmasq这类软件配合实现,dnsmasq可将出口域名的解析地址动态加入ip集合,以匹配策略,禁止联网,则可以通过关闭openwrt由lan区到wan区的伪装功能即可,临时线路的实现可以通过向预设的IP集合中增删指定源IP来满足临时需求,由于 nftables 原生命令对操作集合不友好,本文自实现一个原ipset习惯的python脚本转换。

基础软件部署

操作系统选用当下最新稳定版 OpenWrt 23.05.2 ,另外这里多聊下,为何要坚持选择openwrt,最大的一个好处是,你不用担心磁盘莫名其妙的增大,而又不知道清理哪些东西,避免隔断时间网关宕机事故发生。

安装好必备的基础软件

opkg update && opkg remove dnsmasq && rm -rf /etc/config/dhcp && opkg install dnsmasq-full kmod-nft-tproxy python3
modprobe nft_tproxy

自定义路由表

echo "100 tunnel" >> /etc/iproute2/rt_tables

家宽直连实现

在网络防火墙处取消lan区到wan区的伪装按钮,保存并应用。

截屏2024-01-21 22.03.29.png

新建一个 ext0_list 的ip集合,用来存放需要直连互联网的源ip

截屏2024-01-21 22.06.02 1.png

新建一个nft文件 /etc/vnet/fw4.nft 来存储自定义nftables规则,实现原理是相当于给系统默认的nftables打补丁,引入我们自己的策略,也是为了复用系统图形控制面板的能力。通过系统图形面板控制的table名为fw4,协议族采用的是支持ipv4和ipv6的inet。

#!/usr/sbin/nft -f

table inet fw4 {
    chain srcnat {
        ip saddr @ext0_list jump handle_ext0
    }
        
    chain handle_ext0 {
        meta nfproto ipv4 masquerade comment "!fw4: Masquerade ext0_list to wan traffic" 
    }
}

可以使用如下命令进行执行,根据报错再调整。

nft -f /etc/vnet/fw4.nft

安全测试拦截代理

由于burpsuite默认支持的是http代理,先用xray做一次转化,以便nftables可以接入,xray做形如下配置,bp地址和端口需自定义。

{  
    "inbounds": [  
      {  
        "tag": "BP-REDIRECT",  
        "listen": "127.0.0.1",  
        "port": 10011,  
        "protocol": "dokodemo-door",  
        "settings": {  
          "network": "tcp,udp",  
          "followRedirect": true  
        },  
        "sniffing": {  
          "enabled": true,  
          "destOverride": [  
            "http",  
            "tls"  
          ]  
        },  
        "streamSettings": {  
          "sockopt": {  
            "tproxy": "tproxy"  
          }  
        }  
      }  
    ],  
    "routing": {  
        "domainStrategy": "IPIfNonMatch",  
        "rules": [  
            {  
                 "type": "field",  
                  "inboundTag": "BP-REDIRECT",  
                  "outboundTag": "OUT-BP"  
            }  
        ]  
    },  
    "outbound": {  
        "protocol": "blackhole",  
        "settings": {}  
    },  
    "outboundDetour": [  
        {  
            "tag": "OUT-BP",  
            "protocol": "http",  
            "settings": {  
                "servers": [  
                    {  
                        "address": "10.x.x.2",  
                        "port": 8080  
                    }  
                ]  
            }  
        }  
    ]  
}

随后在nftables侧做导流操作。同样的为需要引流的客户端机器创建一个源IP集合,这里名叫bp_list。

截屏2024-01-21 22.25.09.png

nft配置如下

#!/usr/sbin/nft -f

table inet fw4 {
    chain prerouting {
        ip saddr @bp_list jump handle_bp
    }
    chain handle_bp {
        meta protocol ip meta l4proto tcp tproxy ip to 127.0.0.1:10011 meta mark set 1
    }
}

如上对路由包做的标记为 1 ,对符合标记的做路由策略,在开机脚本/etc/rc.local下追加如下内容

ip route add local default dev lo table 100
ip rule add fwmark 1 table 100

形如下

截屏2024-01-21 22.30.49.png

域名白名单模式

对于这项需求,之前自己写了一个脚本,来动态解析域名所有的IP,添加到对应ip集合,最终因不稳定,而移除。现基于dnmasq来实现。新版的opwenwrt,尽量通过指定的配置文件进行,不要直接去动 /etc/dnsmasq.conf ,不然死活不生效。在/etc/dhcp文件中指定一个配置目录/etc/vnet/dnsmasq/conf.d,如下图

截屏2024-01-21 22.35.53.png

比如我现在要想给生产力机器允许访问网易云音乐相关的域名,我需要准备2个ip集合,一个源地址集合,netease_src_list,一个目的地址集合netease_dst_list,目的地址集合不需要维护,它由dnsmasq自动管理。

同样的,在图形界面创建这2个IP集合

截屏2024-01-21 22.40.32.png

dnsmasq规则文件/etc/vnet/dnsmasq/conf.d/rule.conf做如下配置

# netease list
nftset=/163.com/4#inet#fw4#netease_dst_list
nftset=/netease.com/4#inet#fw4#netease_dst_list
nftset=/126.net/4#inet#fw4#netease_dst_list

重启dnsmasq使其生效

/etc/init.d/dnsmasq restart

nft规则如下

#!/usr/sbin/nft -f

table inet fw4 {
    chain srcnat {
        ip saddr @netease_src_list ip daddr @netease_dst_list jump handle_ext0
    }
    chain handle_ext0 {
        meta nfproto ipv4 masquerade comment "!fw4: Masquerade ext0_list to wan traffic" 
    }
}

整合nft规则,最终 /etc/vnet/fw4.nft 的内容如下

#!/usr/sbin/nft -f

table inet fw4 {
    chain prerouting {
        ip saddr @bp_list jump handle_bp
    }
    
    chain srcnat {
        ip saddr @ext0_list jump handle_ext0
        ip saddr @netease_src_list ip daddr @netease_dst_list jump handle_ext0
    }
        
    chain handle_ext0 {
        meta nfproto ipv4 masquerade comment "!fw4: Masquerade ext0_list to wan traffic" 
    }
}

经过多次测试发现,如果将自定义的nft规则放到开机执行脚本下,容易执行无效,这里使用计划任务来守护,每分钟检测tproxy关键字,没有该关键字自动执行一次,确保策略生效。

* * * * *       /usr/sbin/nft list ruleset | grep -q tproxy || /usr/sbin/nft -f /etc/vnet/fw4.nft

IP集合简化管理

个人比较习惯以前的ipset进行管理,这里提供一个转化脚本/etc/vnet/ipset

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Project : tools
# @File : ipset.py
# @Software: PyCharm
# @Author : 易雾君
# @Email : [email protected]
# @公众号 : 易雾山庄
# @Site : https://www.evling.tech
# @Describe : 家庭基建,生活乐享. 
# @Time : 2024/1/16 21:05


import sys
import subprocess
import json
from copy import deepcopy

'''
nft list set inet fw4 ext0_list
nft add element inet fw4 ext0_list { 172.16.15.6  }
nft delete element inet fw4 ext0_list { 172.16.15.6  }
nft -j list sets
'''

def get_lines():
    output = subprocess.check_output('nft -j list sets'.split()).decode('utf-8')
    return [{x.get('set').get('name'): x.get('set').get('elem')} for x in json.loads(output).get('nftables') if
             x.get('set') and x.get('set').get('name')]

def get_set_elems(set_name):
    result = list()
    try:
        output = subprocess.check_output(f'nft -j list set inet fw4 {set_name}'.split()).decode('utf-8')
        result = json.loads(output).get('nftables')[1].get('set').get('elem', [])
    except subprocess.CalledProcessError as e:
        out_bytes = e.output  # Output generated before error
        code = e.returncode  # Return code
    return result


output = subprocess.check_output('nft -j list sets'.split()).decode('utf-8')
lines = ['ext0_list', 'bp_list']
cmds = list()
ips = list()
if len(sys.argv) > 3:
    ips = sys.argv[3:]


if sys.argv[1] == 'list':
    if len(sys.argv) == 2:
        print(json.dumps(get_lines(), indent=4, ensure_ascii=False))
    if len(sys.argv) > 2:
        print(json.dumps({sys.argv[2]: get_set_elems(sys.argv[2])}, indent=4, ensure_ascii=False))
elif sys.argv[1] == 'add':
    if sys.argv[2] in lines:
        for line in lines:
            set_elems = get_set_elems(line)
            if sys.argv[2] == line:
                to_add_list = [x for x in ips if x not in set_elems]
                if len(to_add_list) == 0:
                    continue
                cmds.append(f'nft add element inet fw4 {line} {{{",".join(to_add_list)}}}')
            else:
                to_del_list = [x for x in ips if x in set_elems]
                if len(to_del_list) == 0:
                    continue
                cmds.append(f'nft delete element inet fw4 {line} {{{",".join(to_del_list)}}}')
    else:
        set_elems = get_set_elems(sys.argv[2])
        to_add_list = [x for x in ips if x not in set_elems]
        if len(to_add_list) > 0:
            cmds.append(f'nft add element inet fw4 {sys.argv[2]} {{{",".join(to_add_list)}}}')
            
elif sys.argv[1] == 'del':
    set_elems = get_set_elems(sys.argv[2])
    to_del_list = [x for x in ips if x in set_elems]
    if len(to_del_list) > 0:
        cmds.append(f'nft delete element inet fw4 {sys.argv[2]} {{{",".join(to_del_list)}}}')
for cmd in cmds:
    try:
        output = subprocess.check_output(cmd.split()).decode('utf-8')
    except subprocess.CalledProcessError as e:
        out_bytes = e.output  # Output generated before error
        code = e.returncode  # Return code
    if output.strip() != '':
        print(output)

在系统执行目录建一个软连接,便于直接使用命令

chmode +x /etc/vnet/ipset
ln -sf /etc/vnet/ipset /usr/bin/ipset

假定临时要对 10.32.24.3 进行系统升级,添加联网

ipset add ext0_list 10.32.24.3

升级完系统,可以通过手动命令禁网

ipset del ext0_list 10.32.24.3

当然,我家里的网关所在母机,每天都会重启一次,我忘了禁网,第二天也会自动恢复到禁网状态。

结语

至此,前文所有需求皆以实现,达成按需策略联网,以纵深防御的思想去做网络管理,避免家庭网络遭受不必要的损失。


评论