49从零开始用Rust编写nginx,我竟然在同一个端口上绑定了多少IP

wmproxy

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

项目地址

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

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

设计目标

快速的设置多IP绑定,及IP端口段的支持,方便快速的自定义能力。

IP解析示例

以下是常见的IP解析示例情况,本地ip为192.168.0.100示例:

  • 正常IP解析

    • 127.0.0.1:8869 解析成 ipv4 127.0.0.1 端口 8869,只接受本地来的连接信息
    • 0.0.0.0:8869 解析成 ipv4 0.0.0.0 端口 8869,可接受所有来自ipv4的连接信息
  • :开头的地址,且不包含-

    • :8869 解析成 ipv4 127.0.0.1 端口 8869 及 ipv4 192.168.0.100 端口 8869
  • 包含-的地址

    • :8869-:8871 解析成 ipv4 127.0.0.1 端口 8869 - 8871 三个端口地址 及 ipv4 192.168.0.100 端口 8869 - 8871 三个端口地址,总共6个端口地址
    • 127.0.0.1:8869-:8871 解析成 ipv4 127.0.0.1 端口 8869 - 8871 三个端口地址 总共3个端口地址
    • 127.0.0.1:8869-192.168.0.100:8871 解析成 ipv4 127.0.0.1 端口 8869 - 8871 三个端口地址 总共3个端口地址,忽略后面的地址,只接受端口号
  • 手动多个地址,可以空格或者,做间隔

    • 127.0.0.1:8869 127.0.0.1:8899 192.168.0.100:8899 就相应的解析成三个端口地址

定义类

由于解析出来的地址可能是多个或者个单个,这里用数组来进行表示

rust 复制代码
#[derive(Debug, Clone)]
pub struct WrapVecAddr(pub Vec<SocketAddr>);

通常序列化会用到FromStr将字符串转化成类

反序列化都会用到Display将类转化成字符串

所以在这里,我们将实现FromStrDisplay

rust 复制代码
impl FromStr for WrapVecAddr {
    type Err = AddrParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // 范围的如:8080-:8090, 表示11端口
        if s.contains("-") {
            let vals = s
                .split(&['-'])
                .filter(|s| !s.is_empty())
                .collect::<Vec<&str>>();
            let start = parse_socker_addr(vals[0])?;
            if vals.len() != 2 {
                return Ok(WrapVecAddr(start));
            } else {
                let end = parse_socker_addr(vals[1])?;
                let mut results = vec![];
                for port in start[0].port()..=end[1].port() {
                    for idx in &start {
                        let mut addr = idx.clone();
                        addr.set_port(port);
                        results.push(addr);
                    }
                }
                return Ok(WrapVecAddr(results));
            }
        } else {
            let vals = s
                .split(&[',', ' '])
                .filter(|s| !s.is_empty())
                .collect::<Vec<&str>>();
            let mut results = vec![];
            for s in vals {
                results.extend(parse_socker_addr(s)?);
            }
            Ok(WrapVecAddr(results))
        }
    }
}

impl Display for WrapVecAddr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut values = vec![];
        for a in &self.0 {
            values.push(format!("{}", a));
        }
        f.write_str(&values.join(","))
    }
}

这样子后我们将配置加上就可以自动实现序列化及反序列化了

rust 复制代码
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    #[serde_as(as = "DisplayFromStr")]
    pub bind_addr: WrapVecAddr,
    // ...   
}

获取本机地址

通过库local-ip-address获取本地IP地址,再根据缺省IP时添加本地IP地址访问,因为缺省时有可能是需要本地内网进行访问。所以需要补上本地网卡地址。所以我们在解析以:时做了特殊处理:

rust 复制代码
fn parse_socker_addr(s: &str) -> Result<Vec<SocketAddr>, AddrParseError> {
    if s.starts_with(":") {
        let port = s.trim_start_matches(':');
        let mut results = vec![];
        if let Ok(port) = port.parse::<u16>() {
            if let Ok(v) = local_ip() {
                results.push(SocketAddr::new(v, port));
            }
            if let Ok(v) = local_ipv6() {
                results.push(SocketAddr::new(v, port));
            }
            results.push(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port));
        } else {
            results.push(format!("127.0.0.1{s}").parse::<SocketAddr>()?);
        }
        Ok(results)
    } else {
        let addr = s.parse::<SocketAddr>()?;
        Ok(vec![addr])
    }
}

参数获取

以下举例file-server的参数

rust 复制代码
#[derive(Debug, Clone, Bpaf)]
#[allow(dead_code)]
struct FileServerConfig {
    /// 静态文件根目录路径
    #[bpaf(short, long, fallback(String::new()))]
    pub(crate) root: String,
    #[bpaf(
        short,
        long,
        fallback(WrapVecAddr(vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8869)])),
        display_fallback
    )]
    /// 监听地址
    pub(crate) listen: WrapVecAddr,
    #[bpaf(long)]
    /// 监听地址
    pub(crate) listen_ssl: Option<WrapVecAddr>,
    /// ...   
}

如此我们就可以轻松的用SSL监听及普通的监听添加多个端口的支持。

bash 复制代码
wmproxy file-server --listen :8869-8871

此时我们可以同时监听3个端口均支持文件服务器。此时我们就可以轻松控制多个端口地址。

绑定多个地址

以下是负载均衡中的绑定示例

rust 复制代码
for v in &value.bind_addr.0 {
    if bind_addr_set.contains(&v) {
        continue;
    }
    bind_addr_set.insert(v);
    let url = format!("http://{}", v);
    log::info!("HTTP服务:{},提供http处理及转发功能。", Style::new().blink().green().apply_to(url));
    let listener = Helper::bind(v).await?;
    listeners.push(listener);
    tlss.push(false);
}

for v in &value.bind_ssl.0 {
    if bind_addr_set.contains(&v) {
        continue;
    }
    bind_addr_set.insert(v);
    if !is_ssl {
        return Err(crate::ProxyError::Extension("配置SSL端口但未配置证书"));
    }
    let url = format!("https://{}", v);
    log::info!("HTTPs服务:{},提供https处理及转发功能。", Style::new().blink().green().apply_to(url));
    let listener = Helper::bind(v).await?;
    listeners.push(listener);
    tlss.push(is_ssl);
}

支持ssl绑定及非ssl绑定同一个location。

其中链接信息使用了console,输出了绿色的,可以点击的url链接。可以方便在启动的时候进行点击。

图中圈圈位置是可以点击跳转成url,方便本地开发环境的时候测试使用。

总结

通过FromStrDisplay的重定义,我们可以支持更强大的自定义的序列化操作,系统绑定端口既认端口号也认绑定IP,所以我们可以对同个端口进行多次绑定。

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