OpenVPN中自定义IP地址分配有两种实现方式,一种是写一个plugin,plugin_call这个调用可以被添加到任何地方,OpenVPN中的plugin挂载点也可在任意位置定义,因此利用plugin来自定义IP地址分配策略就有两种方式,第一种方式是可以新定义一个plugin挂载点,紧接着动态分配IP的代码,然后在plugin中实现策略,当然具体到如何将已经分配的ip地址的指针传入plugin中还需要前文中介绍的方式,将ip地址的内存地址作为环境变量加入到env_set,然后…,这种方式需要修改OpenVPN的代码,要么自己定义一个挂载点,要么在已有的CLIENT_CONNECT中加入新的内存地址环境变量;第二种方式是一种标准的方式,也就是OpenVPN建议的方式,那就是在multi_connection_established中做文章,OpenVPN的代码不需要做任何修改:
static void multi_connection_established (struct multi_context *m, struct multi_instance *mi)
{
...
if (plugin_defined (mi->context.plugins, OPENVPN_PLUGIN_CLIENT_CONNECT)) {
struct argv argv = argv_new (); //下面将要生成一个临时文件的名字,该文件名中包含随机数。
const char *dc_file = create_temp_filename (mi->context.options.tmp_dir, "cc", &gc); //由于这个将要生成的文件只在此挂载点被使用一次,用来让OpenVPN得到配置信息,比如IP地址或者路由之类,因此用完就要被删除,可以也可能删除失败,因此为了防止别的client连入时误用此文件,因此需要随机生成一个文件,从而使得文件名不可预测。
argv_printf (&argv, "%s", dc_file); //将临时文件名加入plugin的argv参数中
delete_file (dc_file); //既然我们已经得到了需要建立的文件名,删除新建的文件
if (plugin_call (mi->context.plugins, OPENVPN_PLUGIN_CLIENT_CONNECT,&argv...) != OPENVPN_PLUGIN_FUNC_SUCCESS) {
... //multi_client_connect_post中将实现抉择,如果plugin定义了IP地址,那么将在此函数中删除动态分配的ip地址
multi_client_connect_post (m, mi, dc_file, option_permissions_mask, &option_types_found);//此函数会读取由plugin生成的配置文件进行参数配置
...
}
...
}
然后在plugin中这么实现即可:(首先要在open回调函数中注册OPENVPN_PLUGIN_CLIENT_CONNECT挂载点)
OPENVPN_EXPORT int openvpn_plugin_func_v1 (openvpn_plugin_handle_t handle, const int type, const char *argv[], const char *envp[])
{
int rv = -1;
struct plugin_context *context = (struct plugin_context *) handle; //plugin_context自定义,可以包含你需要的任何变量
const char *YYY = get_env ("XXX", envp); //get_env函数是一个遍历匹配的过程,XXX是你希望得到的用于IP分配决策的client端变量,比如可以是证书的CN项,也可以是其真实的ip地址
... //还可以从envp中得到任何别的变量,不仅仅是client端的,只要是env_set中的均可以得到
... //实现虚拟IP分配逻辑,可以通过数据库,可以通过unix-pam机制,...不过记住最终的虚拟ip需要写入argv[1]代表的文件中,也就是OpenVPN传入的那个文件名带有随机数的临时文件中,写法很简单:sprintf(cmd, "echo 'ifconfig-push %s'>>%s", 虚拟ip, dc_file);system(cmd);
}
dc_file中不仅仅可以包含client端的虚拟ip地址信息,任何OpenVPN接受的配置信息都可以,比如说路由信息也是可以的。如此就完成了虚拟IP地址的自定义分配过程,如果不太擅长写C语言,那么还有另一种对等的方式可以使用,这就是写一个脚本:
#!/bin/bash
...
#同样取出需要的环境变量(env_set中的变量),用$XXX取出即可
#可以取很多
...
...可以通过perl/python等链接数据库或者不链接数据库而使用别的策略...
echo ifconfig-push 分配结果 >> $1
echo ... >> $1
#end
是不是更简单?以上脚本的方式在OpenVPN中的依据是:
if (mi->context.options.client_connect_script && cc_succeeded) {
struct argv argv = argv_new ();
const char *dc_file = NULL;
setenv_str (mi->context.c2.es, "script_type", "client-connect");
dc_file = create_temp_filename (mi->context.options.tmp_dir, "cc", &gc);
delete_file (dc_file);
argv_printf (&argv, "%sc %s", mi->context.options.client_connect_script, dc_file, cert);
if (openvpn_execve_check (&argv, mi->context.c2.es, S_SCRIPT, "client-connect command failed")) {
multi_client_connect_post (m, mi, dc_file...);
...
}
最后我们注意一点,那就是和OPENVPN_PLUGIN_CLIENT_CONNECT这个宏对应的还有OPENVPN_PLUGIN_CLIENT_CONNECT_V2,并且在if (plugin_defined (mi->context.plugins, OPENVPN_PLUGIN_CLIENT_CONNECT))前面还有一个deprecated callback的注释,这个注释其实也是我们想办法通过在env_set中添加一个IP地址内存地址的原因之一,正如注释所说,plugin的OPENVPN_PLUGIN_CLIENT_CONNECT挂载点是通过一个临时文件来传输信息的,而读写文件是一件很麻烦的事情,整个读写过程不被OpenVPN地址空间监视,除了基于文件系统本身的系统调用测试,你甚至不知道文件建立/删除成功了没有,进程之外的IO行为很容易被别的进程截获,更改,另外,文件IO是一个很耗时的过程,在大负载下会降低性能,更何况,OpenVPN当前是一个串行的大进程,本来的设计缺陷加上每个client一两次文件IO更是雪上加霜。鉴于上述问题,OpenVPN推出了V2系列plugin挂载点,仅针对client连接这个挂载点来说,V2系列的plugin回调函数多了一个传出参数struct plugin_return,更改了V1系列挂载点plugin对env_set可读不可写的缺陷:
struct plugin_return { //返回参数,也就是plugin的传出参数
int n;
struct openvpn_plugin_string_list *list[MAX_PLUGINS];
};
struct openvpn_plugin_string_list { //传出参数容器中的元素,链接成一个链表
struct openvpn_plugin_string_list *next;
char *name;
char *value;
};
if (plugin_defined (mi->context.plugins, OPENVPN_PLUGIN_CLIENT_CONNECT_V2)) {
struct plugin_return pr; //定义一个传出参数
plugin_return_init (&pr);
if (plugin_call (mi->context.plugins, OPENVPN_PLUGIN_CLIENT_CONNECT_V2, NULL, &pr, mi->context.c2.es) != OPENVPN_PLUGIN_FUNC_SUCCESS) {
...//下面的调用中接着调用plugin_return_get_column来搜集被plugin配置或者更改的信息,然后用options_string_import将之配置入OpenVPN的变量中
multi_client_connect_post_plugin (m, mi, &pr, option_permissions_mask, &option_types_found);
...
}
static void multi_client_connect_post_plugin (...) {
struct plugin_return config;
plugin_return_get_column (pr, &config, "config"); //搜集被触及的变量
/* Did script generate a dynamic config file? */ //一看就知道此等代码的作者也少不了复制-粘贴
if (plugin_return_defined (&config)) {
for (i = 0; i < config.n; ++i) { //遍历每一个被触及的变量
if (config.list[i] && config.list[i]->value)
options_string_import (...); //将plugin中触及的变量转化为内部数据结构的字段
}
...
}
plugin的方式有V2系列调用,而脚本的方式却没有,因为脚本本身就在独立的进程地址空间中运行,除了使用共享内存之外不可能有脚本“传出参数”一说的,综合对比文件和共享内存传输数据的复杂性和性能,还是不要使用共享内存了,所以依然使用文件。
实现自定义分配IP地址的方式除了plugin以及脚本方式之外还有很多,实际上动态分配IP只是分配虚拟IP地址的一种方式罢了,在很多应用中,管理client比较重要,因此很多时候都是自定义的虚拟IP地址分配而不是通常我们自己玩时的动态IP地址分配。