缘起

  我们小创业公司使用的宽带是个人家庭宽带(申请企业宽带的成本很高,每月几千块),公网IP过一段时间就会变动,平时使用都没有什么影响,只有一点,阿里云上的服务都配置了安全组,只允许阿里云内网或者白名单IP访问,从而保障公司服务安全性。公司公网IP会动态变更,每一次变更之后都要修改安全组的ip配置,十分麻烦。

  来自攻城师的思考:每一次IP变动都打断了大家专心工作的心流(Flow)状态,而且登录阿里云控制台修改也很繁琐,修改安全组配置的IP名单,能不能自动化?

  大概瞄了一下阿里云相关的API,这事情可以搞。

动手实现

看看需要实现哪些功能:

  • 定时调度
  • 获取公司宽带的公网IP地址
  • 调用阿里云API刷新安全组的白名单IP

如何获取公司宽带的公网IP地址?

这个简单,市场上有很多ip查询的网站,比如 ip.cn,ipip.net ,你访问他们的网站,默认就会显示你的公网ip。

调用阿里云API没什么难度,只是工作量。

核心逻辑就是,获取当前公司ip,存储下来,每隔N分钟,再次获取公司当前ip,与之前记录对比,如果不同,则调用阿里云api,更新安全组白名单IP,最后存储新IP。否则什么也不做。

于是我创建了一个SpringBoot应用,为什么要创建一个java web应用?首先因为最熟悉java,其次是工程创建快速,使用简单,之前我实现了一个模版工程,可以快速创建SpringBoot应用,而且配置了常用的类库,非常方便。

一些核心功能实现:

@Component
public class ScheduleComp {

    Logger logger = LoggerFactory.getLogger(ScheduleComp.class);

    @Autowired
    RefreshSecurityRulService securityRulService;

    /**
     * 每隔3分钟检查一下
     */
    @Scheduled(cron = "0 */3 * * * ?")
    public void cornJob() {
        logger.warn(" >>cron执行....");
        securityRulService.refreshAll(false);
    }
}

securityRulService 的refreshAll()方法

public void refreshAll(boolean force) {
        String nowIp = getLocalIp();
        if (StringUtils.isEmpty(nowIp)) {
            logger.error("get current ip is empty! try next time...");
            return;
        }
        String oldIp = getCurrentIp();
        if (StringUtils.isEmpty(oldIp)) {
            throw new RuntimeException("current ip data is empty!");
        }
        if (StringUtils.equals(nowIp, oldIp) && !force) {
            logger.warn("same ip, nothing todo");
            return;
        }
        ecsRefreshSecurityRuleService.refreshAll(nowIp, oldIp);

        updateCurrentIp(nowIp);
    }

getLocalIp()的实现:

public static String getLocalIp() {
        String ip = getIpCN();
  			...
          
        return ip;
    }

public static String getIpCN() {
        //<p>您现在的 IP:<code>114.0.1.2</code>
        return getLocalIp("http://ip.cn", "<p>您现在的 IP:<code>");
    }


public static String getLocalIp(String url, String prefix) {
        Request request = new Request.Builder()
                .get()
                .url(url)
                .build();
        try {
            Response response = okHttpClient.newCall(request).execute();
            if (response.isSuccessful()) {
                String regexPatten = prefix + "((?:(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d))).){3}(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d))))";
                Pattern pattern = Pattern.compile(regexPatten);
                String resp = response.body().string();
                Matcher matcher = pattern.matcher(resp);
                if (matcher.find()) {
                    String matchStr = matcher.group(0);
                    return matchStr.replace(prefix, "");
                }
            }
            response.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

这里通过 ip.cn 获取本机的公网地址。

还可以配置其他网站,防止ip.cn挂掉而导致服务失败。

getCurrentIp 的实现很简单,从本地文件读取上次记录的ip地址。

public String getCurrentIp() {
        File file = new File("ip.txt");
        if (!file.exists()) {
            throw new RuntimeException("cannot find ip data!");
        }
        try {
            BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(new FileInputStream(file)));
            String strLine = bufferedReader.readLine();
            bufferedReader.close();
            return strLine.trim();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

采用文件存储是最简单直接的方式。更新文件内容的代码:

private void updateCurrentIp(String ip) {
        if (StringUtils.isEmpty(ip))
            return;
        File file = new File("ip.txt");
        file.delete();
        try {
            file.createNewFile();
            BufferedWriter bufferedWriter =
                    new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
            bufferedWriter.write(ip.trim());
            bufferedWriter.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

刷新阿里云安全组配置的逻辑:

public void refreshAll(String nowIp, String oldIp) {

        List<SecurityRuleDO> newSecurityRuleDOList = new ArrayList<>(testServer(nowIp));
        for (SecurityRuleDO securityRuleDO : newSecurityRuleDOList) {
            refresh(securityRuleDO);

        }

        if (!StringUtils.equals(nowIp, oldIp)) {
            List<SecurityRuleDO> oldSecurityRuleDOList = new ArrayList<>();
            oldSecurityRuleDOList.addAll(testServer(oldIp));
            for (SecurityRuleDO securityRuleDO : oldSecurityRuleDOList) {
                deleteOld(securityRuleDO);
            }

        }
    }

private List<SecurityRuleDO> testServer(String ip) {
        List<SecurityRuleDO> securityRuleDOList = new ArrayList<>();
        SecurityRuleDO securityRuleDO = new SecurityRuleDO();
        securityRuleDO.sourceIp = ip;
        securityRuleDO.groupId = "YourECSGroupId";
        securityRuleDO.ipProtocol = "tcp";
        securityRuleDO.portRange = "yourPort/yourPort";
        securityRuleDOList.add(securityRuleDO);
				...
        return securityRuleDOList;
    }


public boolean refresh(SecurityRuleDO securityRuleDO) {
        AuthorizeSecurityGroupRequest request =
                new AuthorizeSecurityGroupRequest();
        request.setSecurityGroupId(securityRuleDO.groupId);
        request.setIpProtocol(securityRuleDO.ipProtocol);
        request.setPortRange(securityRuleDO.portRange);
        request.setNicType(securityRuleDO.nicType);
        request.setPolicy("accept");
        request.setSourceCidrIp(securityRuleDO.sourceIp);

        try {
            AuthorizeSecurityGroupResponse response = client.getAcsResponse(request);
            String requestId = response.getRequestId();
            System.out.println(requestId);
            return true;
        } catch (ClientException e) {
            e.printStackTrace();
            return false;
        }
    }

public boolean deleteOld(SecurityRuleDO securityRuleDO) {
        RevokeSecurityGroupRequest request = new RevokeSecurityGroupRequest();
        request.setSecurityGroupId(securityRuleDO.groupId);
        request.setIpProtocol(securityRuleDO.ipProtocol);
        request.setPortRange(securityRuleDO.portRange);
        request.setNicType(securityRuleDO.nicType);
        request.setPolicy("accept");
        request.setSourceCidrIp(securityRuleDO.sourceIp);

        try {
            RevokeSecurityGroupResponse response = client.getAcsResponse(request);
            String requestId = response.getRequestId();
            System.out.println(requestId);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

其中client的初始化:

DefaultProfile profile = DefaultProfile.getProfile(reginId, appkey, secret);
                    client = new DefaultAcsClient(profile);

小问题

定时3分钟刷新有个小问题,就是有极小的概率,发生IP变更但是还没有及时刷新安全组的配置。怎么办,再加一个主动刷新的接口呗:

@RestController
public class RefreshSecurityRuleController {
    @Autowired
    RefreshSecurityRulService securityRulService;

    @GetMapping("/ip")
    public String getMyIp() {
        return securityRulService.getLocalIp();
    }

    @GetMapping("/fresh")
    public String forceFresh() {
        securityRulService.refreshAll(true);
        return "done!";
    }

}

其中的/fresh强制刷新IP。还附赠了一个接口,获取公司当前的IP。

这个小应用部署在公司局域网内,一直工作的很好,再也不用烦心IP变动这些琐事。

直到有一次,ip.cn 网站挂了。挂了很久,我们就增加了从ip.tool.chinaz.com 网站获取IP。

收工。