본문으로 바로가기

728x90

네트워크 방화벽 등으로 인해 외부로 접속이 불가능한 상황이 있을 수 있습니다. 아래의 예제는 접속이 상호 가능한 장치를 통하여 TCP연결을 포워딩하는 예시입니다. 아래의 코드는 특별하게 어려운 부분은 없어 설명을 최소화합니다.

 

초기 MySQL / MariaDB를 접속하기 위해 MySQL Proxy, MySQL Router, ProxySQL 등을 찾아서 검색을 해보고 직접 설정을 하려 했는데 중간 시스템이 복잡해지고 설치하는 기능들 또한 배보다 배꼽이 커지는 상황이 벌어져서 인터넷검색을 참고하여 아래와 같이 직접 구현하였습니다. 참고한 사이트에 properties 파일을 추가하여 여러 개의 접속을 지원가능하도록 추가하였습니다. 접속자에 대한 제어(접속자 IP기반)가 필요한 경우 접근제어가 적절하게 추가될 부분에 주석이 있으니 참고하시기 바랍니다.

 

TCP Socket Forwarding(Tunneling) 구현하기

Application.java : 실행코드

환경설정파일을 읽고 설정에 따라 소켓서버를 구동합니다.

package kr.ejsoft.socket.forward;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;

public class Application {
	public static void main(String[] args) {
		String propertiesFile = "forward.properties";
		if(args != null && args.length > 0) {
			propertiesFile = args[0];
		}
		
		Properties prop = new Properties();
		File file = null;
		FileInputStream stream = null;
		try {
			file = new File(propertiesFile);
			stream = new FileInputStream(file);
			prop.load(stream);
		} catch (FileNotFoundException e) {
			System.out.println("File Not Found. " + ((file != null) ? file.getAbsolutePath() : ""));
			// e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(stream != null) try { stream.close(); } catch(Exception e) { }
		}
		
		String keynames = prop.getProperty("forward.keys", "");
		String[] keys = keynames.split(",");
		for(String key : keys) {
			if(key == null || "".equals(key.trim())) continue;

			String host = prop.getProperty("forward." + key.trim() + ".host", "").trim();
			String listen1 = prop.getProperty("forward." + key.trim() + ".listen", "0");
			String port1 = prop.getProperty("forward." + key.trim() + ".port", "0");
			
			int listen = -1;
			int port = -1;
			try { listen = Integer.parseInt(listen1); } catch(Exception e) { };
			try { port = Integer.parseInt(port1); } catch(Exception e) { };
			if(port <= 0) port = listen;
			
			if(host != null && !"".equals(host.trim()) && listen > 0) {
				new SocketServer(listen, host, port).start();
			}
		}
	}
}

 

SocketServer.java : 서버소켓

클라이언트의 접속을 위해 서버 소켓을 열고 접속을 대기합니다. 클라이언트 접속시에 접근제어를 적절하게 구현할 수 있습니다.

package kr.ejsoft.socket.forward;

import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SocketServer {
	private int listen = -1;
	private String host = null;
	private int port = -1;
	
	public SocketServer(int listen, String host, int port) {
		this.listen = listen;
		this.host = host;
		this.port = port;
	}

	public void start() {
		try {
			// 서버소켓을 생성하고 5000번 포트와 결합(bind) 시킨다.
			ServerSocket serverSocket = new ServerSocket(this.listen);
			System.out.println(getTime() + " 서버(" + this.host + ":" + this.port + ")가 준비되었습니다.");

			while (true) {
				// 서버소켓은 연결요청이 올 때까지 실행을 멈추고 기다린다.
				// System.out.println(getTime() + " 서버가 대기중입니다.");
				// 서버소켓의 연결을 허용합니다.
				Socket clientSocket = serverSocket.accept();
				
				// String clientIp = clientSocket.getInetAddress().getHostAddress();
				// IP를 통해 권한을 부여하려면 여기에 코드를 추가하십시오.
//				if(denied) {
//					try { clientSocket.close(); } catch (Exception e) { }
//					break;
//				}
				
				ClientThread clientThread = new ClientThread(clientSocket, this.host, this.port);
				clientThread.start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	static String getTime() {
		SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
		return f.format(new Date());
	}
}

 

ClientThread.java : 클라이언트 쓰레드

접속된 클라이언트를 쓰레드를 통하여 동적으로 처리합니다.

package kr.ejsoft.socket.forward;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class ClientThread extends Thread {
	private final Socket clientSocket;
	private Socket serverSocket;
	private boolean forwardingActive = false;
	private String host = null;
	private int port = -1;

	public ClientThread(Socket clientSocket, String host, int port) {
		this.clientSocket = clientSocket;
		this.host = host;
		this.port = port;
	}

	@Override
	public void run() {
		InputStream clientInput;
		OutputStream clinetOutput;

		InputStream serverInput;
		OutputStream servierOutPut;

		try {
			serverSocket = new Socket(this.host, this.port);
			serverSocket.setKeepAlive(true);
			clientSocket.setKeepAlive(true);

			clientInput = clientSocket.getInputStream();
			clinetOutput = clientSocket.getOutputStream();

			serverInput = serverSocket.getInputStream();
			servierOutPut = serverSocket.getOutputStream();
		} catch (Exception e) {
			System.out.println("Can not Connect to " + this.host + ":" + this.port);
			connectionBroket();
			return;
		}

		forwardingActive = true;
		ForwardThread clientForward = new ForwardThread(this, clientInput, servierOutPut);
		clientForward.start();

		ForwardThread serverForward = new ForwardThread(this, serverInput, clinetOutput);
		serverForward.start();

		System.out.println(
				"TCP Forwarding "
				+ clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort()
				+ " <--> "
				+ serverSocket.getInetAddress().getHostAddress() + ":" + serverSocket.getPort()
				+ " START");
	}

	public void connectionBroket() {
		try { serverSocket.close(); } catch (Exception e) { }
		try { clientSocket.close(); } catch (Exception e) { }

		if (forwardingActive) {
			System.out.println(
					"TCP Forwarding "
					+ clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort()
					+ " <--> "
					+ serverSocket.getInetAddress().getHostAddress() + ":" + serverSocket.getPort()
					+ " STOP");
			forwardingActive = false;
		}
	}
}

 

ForwardThread.java : 데이터 처리 쓰레드

입출력되는 데이터를 각각의 연결에 포워딩합니다.

package kr.ejsoft.socket.forward;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class ForwardThread extends Thread {
	private static final int BUFFER_SIZE = 8192;

	InputStream mInputStrean;
	OutputStream mOutPutStream;
	ClientThread mParent;

	public ForwardThread(ClientThread aParent, InputStream aInputStream, OutputStream aOutPutStream) {
		mParent = aParent;
		mInputStrean = aInputStream;
		mOutPutStream = aOutPutStream;
	}

	@Override
	public void run() {
		byte[] buffer = new byte[BUFFER_SIZE];

		try {
			while (true) {
				int bytesRead = mInputStrean.read(buffer);
				if (bytesRead == -1) {
					break;
				}
				mOutPutStream.write(buffer, 0, bytesRead);
				mOutPutStream.flush();
			}
		} catch (IOException e) {
		}
		mParent.connectionBroket();
	}
}

 

forward.properties : 환경설정

forward.keys : 접속되는 연결 목록의 키를 지정합니다.

forward.key.listen : 접속 대기할 서버소켓 포트를 지정합니다.

forward.key.host : 연결하려는 대상의 호스트 주소를 입력합니다.

forward.key.port : 연결하려는 대상의 포트를 입력합니다. 입력하지 않을 경우 forwar.key.listen 값을 사용합니다.

연결하려는 대상이 여러개 이고 포트가 동일한 경우에는 아래와 같이 listen포트를 여러 개 생성하여 포워딩하도록 구성합니다.

forward.keys=key1, key2

forward.key1.listen = 3306
forward.key1.host = target.host1
forward.key1.port = 3306

forward.key2.listen = 3307
forward.key2.host = target.host2
forward.key2.port = 3306

 

참고사이트

소스코드

본 포스트 관련 소스코드는 여기에서 다운로드 가능합니다.

 

 

 

 

 

 

728x90