misc: 强制一个程序使用一个特定的网卡
如何强制一个程序使用一个特定的网卡
解决之前一直没解决的问题:如何强制一个程序使用一个特定的网卡。假如我的手提电脑同时连着无线wifi和有线的以太网,那么我要如何指示我的程序(比如浏览器)使用wifi还是以太网呢?在默认情况下,一般是自动选择以太网的。但是,如果以太网的连接比较慢,或者你想让某些程序用wifi访问外网,怎么做到呢?今天我尝试了一下。其中原理和操作不难,但是如果要完全验证自己的想法,有些坑(比如修改路由表)是绕不过去的。
假设有线网卡的IP是: 110.56.65.45
, 无线网卡的IP是: 192.168.10.100
。假设在Linux下(Windows下也相似)
Scenario-1
如果你要从头写这个程序,而且,使用了bind()之类的函数,那么你是可以很容易选择你的程序到底要使用那个网卡的。比如一下的python程序(C长得也差不多):
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.bind(("110.56.65.45", 5005)) # or sock.bind(("192.168.10.100",500))
只要在bind()的时候选择那个你想要的IP地址就行(每个网卡都有一个IP)。
但是,假如你是在使用别人的程序时,又如何呢?如果没有源代码,就改不了别人程序了;即使有源代码,改起来可能也会错漏百出。
Scenario-2
有一种方法可以解决这个问题: 使用LD_PRELOAD变量对C标准库的库文件中的bind()函数做一个“overload”
这种解决方法的思路是,通过在链接之前定义LD_PRELOAD环境变量,使得链接器首先链接我们自己实现的bind()函数,这样我们就可以在bind()函数里面动手脚,使得无论第三方程序如何定义或使用bind()函数,我们都可以将其绑定到我们想要的网卡接口,因为在链接的时候,对bind()函数的引用被链接到了我们定义的bind()那里(LD_PRELOAD起作用了)。当然,前提是第三方程序必须是动态链接的(一般都是)。比如可以这样实现:
/*
Copyright (C) 2000 Daniel Ryde
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your optioyinwn) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*/
/*
LD_PRELOAD library to make bind and connect to use a virtual
IP address as localaddress. Specified via the enviroment
variable BIND_ADDR.
Compile on Linux with:
gcc -nostartfiles -fpic -shared bind.c -o bind.so -ldl -D_GNU_SOURCE
Example in bash to make inetd only listen to the localhost
lo interface, thus disabling remote connections and only
enable to/from localhost:
BIND_ADDR="127.0.0.1" LD_PRELOAD=./bind.so /sbin/inetd
Example in bash to use your virtual IP as your outgoing
sourceaddress for ircII:
BIND_ADDR="your-virt-ip" LD_PRELOAD=./bind.so ircII
Note that you have to set up your servers virtual IP first.
This program was made by Daniel Ryde
email: daniel@ryde.net
web: http://www.ryde.net/
TODO: I would like to extend it to the accept calls too, like a
general tcp-wrapper. Also like an junkbuster for web-banners.
For libc5 you need to replace socklen_t with int.
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <dlfcn.h>
#include <errno.h>
int (*real_bind)(int, const struct sockaddr *, socklen_t);
int (*real_connect)(int, const struct sockaddr *, socklen_t);
char *bind_addr_env;
unsigned long int bind_addr_saddr;
unsigned long int inaddr_any_saddr;
struct sockaddr_in local_sockaddr_in[] = { 0chengx };
void _init (void)
{
const char *err;
real_bind = dlsym (RTLD_NEXT, "bind");
if ((err = dlerror ()) != NULL) {
fprintf (stderr, "dlsym (bind): %s\n", err);
}
real_connect = dlsym (RTLD_NEXT, "connect");
if ((err = dlerror ()) != NULL) {
fprintf (stderr, "dlsym (connect): %s\n", err);
}
inaddr_any_saddr = htonl (INADDR_ANY);
if (bind_addr_env = getenv ("BIND_ADDR")) {
bind_addr_saddr = inet_addr (bind_addr_env);
local_sockaddr_in->sin_family = AF_INET;
local_sockaddr_in->sin_addr.s_addr = bind_addr_saddr;
local_sockaddr_in->sin_port = htons (0);
}
}
int bind (int fd, const struct sockaddr *sk, socklen_t sl)
{
static struct sockaddr_in *lsk_in;
lsk_in = (struct sockaddr_in *)sk;
/* printf("bind: %d %s:%d\n", fd, inet_ntoa (lsk_in->sin_addr.s_addr),
ntohs (lsk_in->sin_port));*/
if ((lsk_in->sin_family == AF_INET)
&& (lsk_in->sin_addr.s_addr == inaddr_any_saddr)
&& (bind_addr_env)) {
lsk_in->sin_addr.s_addr = bind_addr_saddr;
}
return real_bind (fd, sk, sl);
}
int connect (int fd, const struct sockaddr *sk, socklen_t sl)
{
static struct sockaddr_in *rsk_in;
rsk_in = (struct sockaddr_in *)sk;
/* printf("connect: %d %s:%d\n", fd, inet_ntoa (rsk_in->sin_addr.s_addr),
ntohs (rsk_in->sin_port));*/
if ((rsk_in->sin_family == AF_INET)
&& (bind_addr_env)) {
real_bind (fd, (struct sockaddr *)local_sockaddr_in, sizeof (struct sockaddr));
}
return real_connect (fd, sk, sl);
}
上面的实现中,我们改写了bind()和connect()接口,于是无论第三方程序使用的bind()和connect()被链接到了我们所定义的bind()和connect()那里了。
它的用法是这样的:
- 首先编译程序
$ gcc -nostartfiles -fpic -shared bind.c -o bind.so -ldl -D_GNU_SOURCE
- 在使用第三方程序时提前做一些手脚,比如
$ BIND_ADDR="192.168.10.100" LD_PRELOAD=./bind.so curl www.baidu.com
这样,就可以强制性地让curl
程序使用192.168.10.100
这个无线网卡接口去访问外网了。
SOME ISSUES
做这个实验并不是那么一帆风顺的。我列举一下我遇到的问题。
关于 LD_PRELOAD
LD_PRELOAD 这个环境变量并不是任何时候都会起作用。在Linux下(我的Linux内核版本是3.19.0),只有当effective user id等于read user id时才会起作用。关于effective user id和real user id可以参考Linux man page或这里。最简单的解决方法是,在root身份下试验。
关于路由表
很多时候,为了验证你的想法,你需要直接更改本机(localhost)的路由表。比如,你想看看,把eth0的路由信息去掉,然后强制使用这个接口,看看是不是真的连不上外网了。假如真的是,那么证明你的实验成功了。
在Linux下,修改路由表可以用route命令。用 *route -n *命令查看当前路由表,用 route add命令增加路由表entry。比如:
# route add -net 127.0.0.1 netmask 255.255.255.0 dev eth0
用route del命令删除路由表的entry。比如:
# route del -net 127.0.0.1 netmask 255.255.255.0 dev eth0
在某个时刻,我的路由表长这样:
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 255.255.255.0 UG 0 0 0 wlan0
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 wlan0
192.168.1.0 192.168.1.1 255.255.255.0 UG 0 0 0 wlan0
192.168.1.0 0.0.0.0 255.255.255.0 UG 0 0 0 wlan0
(上面的路由表是我自己各种折腾弄出来的,一般的路由表不会长这样,为了说明问题而已)‘
可以看到,这个路由表是没有eth0(以太网)相关的路由信息的,所以如果某个程序使用这个接口,那么,按照猜想,必定会失败。
我用curl程序来做实验。在普通情况下, curl www.baidu.com
是直接返回一张网页的,因为,即使有线网卡不行,系统也会自动选择可通行的无线网卡。但是,假如强制使用有线网卡:
LD_PRELOAD=/home/walkerlala/misc/my-bind.so BIND_ADDR="110.56.65.45" curl www.baidu.com
那么curl就不能正常运行了。这就证明了我们已经成功地强制第三方程序(curl)使用了我们指定的网卡。
关于PING
ping程序似乎不能被强制。 to-be-continued...
References
- How to use different network interfaces for different processes?
- Source code of bind.c
- BINDING APPLICATIONS TO A SPECIFIC IP
- How Do I Find Out My Linux Gateway / Router IP Address?
- how to use cURL on specific interface
- RealUID, Saved UID, Effective UID. What's going on?
:)