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()那里了。

它的用法是這樣的:

  1. 首先編譯程序
$ gcc -nostartfiles -fpic -shared bind.c -o bind.so -ldl -D_GNU_SOURCE
  1. 在使用第三方程序時提前做一些手腳,比如
$ 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 idreal 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

  1. How to use different network interfaces for different processes?
  2. Source code of bind.c
  3. BINDING APPLICATIONS TO A SPECIFIC IP
  4. How Do I Find Out My Linux Gateway / Router IP Address?
  5. how to use cURL on specific interface
  6. RealUID, Saved UID, Effective UID. What's going on?






















:)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM