Java也玩透明代理,並使用graalvm編譯成可執行文件。

所謂透明代理就是利用了Linux的IP_TRANSPARENT的特性實現的。對應C代碼爲

setsockopt(fd, SOL_IP, IP_TRANSPARENT, &yes, sizeof(int))

由於jvm對socket的封裝,並不能設置此屬性,因此不能直接實現。所以我們可以通過JNA直接調用C代碼實現此功能。

調用方式很簡單,一行代碼即可

int fd = FDTool.getFD(server);
Native2.setsockopt(fd, Native2.SOL_IP, Native2.IP_TRANSPARENT, 1);

關鍵點在於如何獲取fd的值。由於Jvm沒有開放獲取FD的接口,這裏只能通過反射越權獲取。

	static class FDTool{
		static Field socketFD;
    	static Field fileFD;
    	static Field socktImplFD;
		private static Field socketChannelFD;
		private static Method socketGet;
		private static Method servSocketGet;
		private static Field servSocktImplFD;
		private static Field servSocketChannelFD;
		private static Field datagramChannelFD;
    	
    	static  {
    		try {
    			socketGet =  Socket.class.getDeclaredMethod("getImpl");
    			socketGet.setAccessible(true);
				socktImplFD = Socket.class.getDeclaredField("impl");
				socktImplFD.setAccessible(true);
				socketFD = SocketImpl.class.getDeclaredField("fd");
				socketFD.setAccessible(true);
				fileFD = FileDescriptor.class.getDeclaredField("fd");
				fileFD.setAccessible(true);
				
				
				servSocketGet = ServerSocket.class.getDeclaredMethod("getImpl");
				servSocketGet.setAccessible(true);
				servSocktImplFD = ServerSocket.class.getDeclaredField("impl");
				servSocktImplFD.setAccessible(true);
				
				datagramChannelFD = Class.forName("sun.nio.ch.DatagramChannelImpl").getDeclaredField("fd");
				
				socketChannelFD = Class.forName("sun.nio.ch.SocketChannelImpl").getDeclaredField("fd");
				socketChannelFD.setAccessible(true);
				
				servSocketChannelFD = Class.forName("sun.nio.ch.ServerSocketChannelImpl").getDeclaredField("fd");
				servSocketChannelFD.setAccessible(true);
				
				//localPort0(int fd)
			} catch (Exception e) {
				throw new RuntimeException(e);
			} 
    	}
    	
    	
    	private static int getFD(Socket sock) {
    		try {
				Object impl = socktImplFD.get(sock);
				System.out.println(impl.getClass());
				Object fd = socketFD.get(impl);
				if(fd == null) {
					//初始化一次
					socketGet.invoke(sock);
					fd = socketFD.get(impl);
				}
				System.out.println(fd.getClass());
				return (Integer) fileFD.get(fd);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			} catch (InvocationTargetException e) {
				throw new RuntimeException(e);
			}
    	}
    	
    	private static int getFD(DatagramChannel udp) {
    		try {
				Object fd = datagramChannelFD.get(udp);
				return (Integer) fileFD.get(fd);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			}
    	}
    	
    	private static int getFD(ServerSocket sock) {
    		try {
				Object impl = servSocktImplFD.get(sock);
				System.out.println(impl.getClass());
				Object fd = socketFD.get(impl);
				if(fd == null) {
					//初始化一次
					servSocketGet.invoke(sock);
					fd = socketFD.get(impl);
				}
				System.out.println(fd.getClass());
				return (Integer) fileFD.get(fd);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			} catch (InvocationTargetException e) {
				throw new RuntimeException(e);
			}
    	}
    	
    	
    	
    	public static int getFD(ServerSocketChannel server) {
    		try {
				Object fd = servSocketChannelFD.get(server);
				return (Integer) fileFD.get(fd);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			}
		}

		private static int getFD(SocketChannel sock) {
    		try {
				Object fd = socketChannelFD.get(sock);
				return (Integer) fileFD.get(fd);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			}
    	}
	}

至此,所有的障礙已經打通。

編寫代碼,放到linux上測試。這裏我們利用linux的網絡命名空間創造網絡環境。這裏使用3個命名空間serv、gw、client。網絡結構如下。

#創建3個網絡空間
ip netns add gw
ip netns add serv
ip netns add client
#創建4個網卡a0-a1, b0-b1
ip link add a0 type veth peer name a1
ip link add b0 type veth peer name b1
#分配命名空間
ip link set a0 netns gw
ip link set b0 netns gw
ip link set a1 netns serv
ip link set b1 netns client
ip netns exec gw ip link set lo up
ip netns exec gw ip link set a0 up
ip netns exec gw ip link set b0 up
ip netns exec serv ip link set a1 up
ip netns exec serv ip link set lo up
ip netns exec client ip link set lo up
ip netns exec client ip link set b1 up
#設置ip
ip netns exec gw ip addr add 10.0.7.1/24 dev a0
ip netns exec gw ip addr add 10.0.20.1/24 dev b0
ip netns exec serv ip addr add 10.0.7.100/24 dev a1
ip netns exec client ip addr add 10.0.20.5/24 dev b1
#設置網關路由表
ip netns exec serv ip route add default via 10.0.7.1
ip netns exec client ip route add default via 10.0.20.1

在網關上設置透明代理規則。

#先進入網絡空間
ip netns exec gw bash

#帶有mark1的包都發到table 100
ip rule add fwmark 1 table 100 
#所有包都走lo(本地迴環)
ip route add local 0.0.0.0/0 dev lo table 100 

#所有非本機的數據包,打上mark轉給本機2001端口處理
iptables -t mangle -A PREROUTING -p tcp ! -d 10.0.7.1 -j TPROXY --on-port 2001 --tproxy-mark 0x1/0x1

使用graalvm編譯程序。這裏有個坑,由於使用了JNA加載動態函數庫,因此不能使用靜態編譯。

此處省略5000字。

啓動程序,成功接收非本地ip的數據包,並且使用被代理的非本地ip發送數據。測試成功。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章