38. 干货系列从零用Rust编写负载均衡及代理,负载均衡中ip通行与禁止

wmproxy

wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

设计目标

需要能对针对性的IP地址进行放行或者禁止,从而达到网络限制或者安全等目的,保护系统的整体稳定性。

IP的作用

IP地址的作用是标示一台在互联网上的主机,就像每个人的住宅地址一样,邮寄东西需要住宅地址,而互联网上一台电脑对另一台电脑发送数据也需要一个可以识别的地址。

早期的IP地址由32位(即IPv4)的数据表示,也就是大概有42亿的地址,以现如今的网络拥有量已经将公有IP即将耗尽的情形。即使有大量的地址并不会占用公网地址,如公司内部的电脑,家庭内部的电脑,手机等并未占用公网地址,可能很多的设备占用了同一个公网出口。所以我们做了IPv4的限定的时候,也有可能将正常的用户做了相关限定。

现在正在推行的IP地址是128位(即IPv6)的数据将可以表示很大的IP数,将可以每个物联网的设备都拥有独立的IP,但是由于IP的升级涉及到大量的基础设备,大量的旧软件,所以当下基本上两种IP都必须支持的阶段。但是国内相对IPv4占了大部分。

IP中子网掩码

子网掩码就是为了划分同网段的主机数量。每类网段默认是254个。

我们在现实的生活中经常看到路由上255.255.255.0/24也经常在云的白名单上看到了0.0.0.0/0,那么他们表示的含义又是什么呢?

通常我们可以把IPv4看成一个无符号的32位整型,那么255.255.255.0/24后面的24就表示0xffffff00,那么我们将某个IP与该值进行按位与运算,得到相同的值将表示归属于同一个IP段。

举例:内网常用的ip如192.168.0.100按位与后将变成192.168.0.0,及192.168.0.1-254这254个ip按位与得到的地址均为
192.168.0.0,所以他们归属于同一个IP段,也就是表示在他们在同一个路由器下面。

0.0.0.0/0中最后的0就是表示0x00000000,那么我们将任意的IP与该值做按位与得到的值均为0.0.0.0,则表示所有的IP都归属于同一个类,也就是通常配置的白名单对所有的都生效。

IPv6与IPv4类似,只是IPv6的长度更长,子网的长度可以由0-127,而IPv6不叫子网掩码,通常称其为前缀,但其原理都是用位表示,前n位为网络位,则说明IP只要前n位一样,则子网一样,IP的机制通了,涉及IP的问题也就好解决了。

源码的实现

Rust中并不支持子网掩码等,那么我们将在其基础上增加一个8位的无符号型:

RUST 复制代码
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IpGate {
    pub ip: IpAddr,
    pub gate: u8,
}

序列化我们通过serde_with中的DisplayForStr实现,如果存在/则将其切割,如果不存在那么子网掩码位数为0,兼容两种模式:

RUST 复制代码
impl FromStr for IpGate {
    type Err = io::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let vals = s.split("/").collect::<Vec<&str>>();
        let ip = vals[0].parse::<IpAddr>().map_err(|_| io::Error::new(io::ErrorKind::Other, "parse ip error"))?;
        let mut gate = 0;
        if vals.len() > 1 {
            gate = vals[1].parse::<u8>().map_err(|_| io::Error::new(io::ErrorKind::Other, "parse ip error"))?;
            if ip.is_ipv4() && gate > 32 {
                return Err(io::Error::new(io::ErrorKind::Other, "too big gate"));
            } else if ip.is_ipv6() && gate > 128 {
                return Err(io::Error::new(io::ErrorKind::Other, "too big gate"));
            }
        }
        Ok(IpGate {
            ip, gate
        })
    }
}

impl Display for IpGate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.gate > 0 {
            f.write_fmt(format_args!("{}/{}", self.ip, self.gate))
        } else {
            f.write_fmt(format_args!("{}", self.ip))
        }
    }
}

查看是否包含:

RUST 复制代码
pub fn contains(&self, ip: &IpAddr) -> bool {
    if self.gate == 0 {
        ip == &self.ip
    } else {
        match (&ip, &self.ip) {
            (IpAddr::V4(other), IpAddr::V4(my)) => {
                let other = u32::from_be_bytes(other.octets()) >> (32u8 - self.gate);
                let my = u32::from_be_bytes(my.octets()) >> (32u8 - self.gate);
                other == my
            }
            _ => {
                ip == &self.ip
            }
        }
    }
}

在负载均衡中的通行及禁止

我们将配置信息转化成可通行的IP段数组或者禁止的IP段数组

  • 如果存在可通行的配置那么必须在配置中才可通行
  • 如果存在禁止的IP,那么在配置中的将会被禁止

我们在配置的时候,就可以进行如下的配置:

TOML 复制代码
[[http.server.location]]
rule = "/try"
# 只允许本地网络通行
allow_ip = "127.0.0.1 192.168.0.0/24"

[[http.server.location]]
rule = "/"
reverse_proxy = "http://server"
# 全面禁止10开头的IP段
deny_ip = "10.0.0.0/8"

源码示例:

RUST 复制代码
if l.comm.deny_ip.is_some() || l.comm.allow_ip.is_some() {
    if let Some(ip) = req.headers().system_get("{client_ip}") {
        let ip = ip
            .parse::<IpAddr>()
            .map_err(|_| ProtError::Extension("client ip error"))?;
        if let Some(allow) = &l.comm.allow_ip {
            if !allow.contains(&ip) {
                return Ok(Response::status503()
                    .body("now allow ip")
                    .unwrap()
                    .into_type());
            }
        }
        if let Some(deny) = &l.comm.deny_ip {
            if deny.contains(&ip) {
                return Ok(Response::status503().body("deny ip").unwrap().into_type());
            }
        }
    }
}

小结

后续可能需要在接受连接的时候就直接禁止掉IP,那么我们可以防止客户端持续的发送流量,即可能造成流量被耗尽。

IP的通行及禁止帮我们更好的保护系统的健壮性及私域的隐私性做保证。自动禁止IP的话,将是WAF等进阶能力的,更好的保护源站。

点击 [关注][在看][点赞] 是对作者最大的支持