Skip to content

Commit 8908628

Browse files
committed
feat: 1. 封装tcpmux的通用aes对称加解密代码 2. 初步实现tcpmuxclient
1 parent b5604ec commit 8908628

File tree

9 files changed

+540
-0
lines changed

9 files changed

+540
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package top.meethigher.proxy;
2+
3+
import javax.crypto.Cipher;
4+
import javax.crypto.KeyGenerator;
5+
import javax.crypto.SecretKey;
6+
import javax.crypto.spec.GCMParameterSpec;
7+
import javax.crypto.spec.SecretKeySpec;
8+
import java.nio.ByteBuffer;
9+
import java.security.SecureRandom;
10+
import java.util.Base64;
11+
12+
/**
13+
* 高性能 AES/GCM 对称加解密工具类(仅依赖 JDK 标准库)
14+
*/
15+
public final class FastAes {
16+
17+
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
18+
private static final int AES_KEY_LEN = 128; // bit
19+
private static final int GCM_TAG_LEN = 128; // bit
20+
private static final int GCM_IV_LEN = 12; // byte
21+
22+
private static final SecureRandom RAND = new SecureRandom();
23+
24+
/* 每个线程复用自己的 Cipher 实例 */
25+
private static final ThreadLocal<Cipher> CIPHER_HOLDER = ThreadLocal.withInitial(() -> {
26+
try {
27+
return Cipher.getInstance(TRANSFORMATION);
28+
} catch (Exception e) {
29+
throw new RuntimeException("AES/GCM not available", e);
30+
}
31+
});
32+
33+
private FastAes() {} // utility class
34+
35+
/* ---------------------------------- 对外 API ---------------------------------- */
36+
37+
/**
38+
* 随机生成 AES-128 密钥
39+
*/
40+
public static SecretKey generateKey() {
41+
try {
42+
KeyGenerator kg = KeyGenerator.getInstance("AES");
43+
kg.init(AES_KEY_LEN);
44+
return kg.generateKey();
45+
} catch (Exception e) {
46+
throw new RuntimeException(e);
47+
}
48+
}
49+
50+
/**
51+
* 将原始密钥字节数组包装成 SecretKey
52+
*/
53+
public static SecretKey restoreKey(byte[] rawKey) {
54+
if (rawKey.length != AES_KEY_LEN / 8) {
55+
throw new IllegalArgumentException("Key length != 16 byte");
56+
}
57+
return new SecretKeySpec(rawKey, "AES");
58+
}
59+
60+
/**
61+
* 加密:返回 byte[],格式为 IV(12B) + CipherText + Tag(16B)
62+
*/
63+
public static byte[] encrypt(byte[] plain, SecretKey key) {
64+
try {
65+
byte[] iv = new byte[GCM_IV_LEN];
66+
RAND.nextBytes(iv);
67+
68+
Cipher cipher = CIPHER_HOLDER.get();
69+
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LEN, iv));
70+
71+
byte[] cipherText = cipher.doFinal(plain);
72+
73+
return ByteBuffer.allocate(iv.length + cipherText.length)
74+
.put(iv)
75+
.put(cipherText)
76+
.array();
77+
} catch (Exception e) {
78+
throw new RuntimeException("Encrypt error", e);
79+
}
80+
}
81+
82+
/**
83+
* 解密:输入格式须为 IV(12B) + CipherText + Tag(16B)
84+
*/
85+
public static byte[] decrypt(byte[] ivPlusCipherText, SecretKey key) {
86+
try {
87+
if (ivPlusCipherText.length < GCM_IV_LEN) {
88+
throw new IllegalArgumentException("Bad input length");
89+
}
90+
ByteBuffer buf = ByteBuffer.wrap(ivPlusCipherText);
91+
92+
byte[] iv = new byte[GCM_IV_LEN];
93+
buf.get(iv);
94+
95+
byte[] cipherAndTag = new byte[buf.remaining()];
96+
buf.get(cipherAndTag);
97+
98+
Cipher cipher = CIPHER_HOLDER.get();
99+
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LEN, iv));
100+
101+
return cipher.doFinal(cipherAndTag);
102+
} catch (Exception e) {
103+
throw new RuntimeException("Decrypt error", e);
104+
}
105+
}
106+
107+
/* ----------------------------- 简易 Base64 封装 ----------------------------- */
108+
109+
public static String encryptToBase64(byte[] plain, SecretKey key) {
110+
return Base64.getEncoder().encodeToString(encrypt(plain, key));
111+
}
112+
113+
public static byte[] decryptFromBase64(String base64, SecretKey key) {
114+
return decrypt(Base64.getDecoder().decode(base64), key);
115+
}
116+
117+
}

src/main/java/top/meethigher/proxy/NetAddress.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,13 @@ public boolean equals(Object o) {
3838
public int hashCode() {
3939
return Objects.hashCode(this.toString());
4040
}
41+
42+
public static NetAddress parse(String addr) {
43+
try {
44+
String[] addrArr = addr.split(":");
45+
return new NetAddress(addrArr[0], Integer.parseInt(addrArr[1]));
46+
} catch (Exception e) {
47+
return null;
48+
}
49+
}
4150
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package top.meethigher.proxy.tcp.mux;
2+
3+
import io.vertx.core.Vertx;
4+
import io.vertx.core.buffer.Buffer;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import top.meethigher.proxy.NetAddress;
8+
import top.meethigher.proxy.tcp.tunnel.codec.TunnelMessageCodec;
9+
10+
import javax.crypto.SecretKey;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
15+
import static top.meethigher.proxy.FastAes.*;
16+
17+
/**
18+
* TCP Port Service Multiplexer (TCPMUX)
19+
* <p>
20+
* 单端口多路复用
21+
*
22+
* @author <a href="https://meethigher.top">chenchuancheng</a>
23+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc1078">RFC 1078 - TCP port service Multiplexer (TCPMUX)</a>
24+
* @since 2025/07/26 16:18
25+
*/
26+
public abstract class Mux {
27+
28+
private static final Logger log = LoggerFactory.getLogger(Mux.class);
29+
/**
30+
* 默认对称加密密钥
31+
*/
32+
protected static final String SECRET_DEFAULT = "1234567890abcdef";
33+
34+
protected static final char[] ID_CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
35+
36+
37+
protected final Vertx vertx;
38+
39+
protected final String secret;
40+
41+
42+
public Mux(Vertx vertx, String secret) {
43+
this.vertx = vertx;
44+
this.secret = secret;
45+
}
46+
47+
/**
48+
* 将host与port通过英文冒号连接,返回加密base64串(无换行)
49+
*/
50+
public Buffer encode(List<NetAddress> netAddresses) {
51+
String[] array = new String[netAddresses.size()];
52+
for (int i = 0; i < array.length; i++) {
53+
array[i] = netAddresses.get(i).toString();
54+
}
55+
String addr = String.join(",", array);
56+
SecretKey key = restoreKey(secret.getBytes(StandardCharsets.UTF_8));
57+
String encryptedAddr = encryptToBase64(addr.getBytes(StandardCharsets.UTF_8), key);
58+
return TunnelMessageCodec.encode((short) 0, encryptedAddr.getBytes(StandardCharsets.UTF_8));
59+
}
60+
61+
/**
62+
* 将加密内容还原
63+
*/
64+
public List<NetAddress> decode(Buffer buffer) {
65+
TunnelMessageCodec.DecodedMessage decode = TunnelMessageCodec.decode(buffer);
66+
String encryptedAddr = new String(decode.body, StandardCharsets.UTF_8);
67+
SecretKey key = restoreKey(secret.getBytes(StandardCharsets.UTF_8));
68+
String addr = new String(decryptFromBase64(encryptedAddr, key),
69+
StandardCharsets.UTF_8);
70+
List<NetAddress> list = new ArrayList<>();
71+
if (addr.contains(",")) {
72+
String[] split = addr.split(",");
73+
for (String s : split) {
74+
add(s, list);
75+
}
76+
} else {
77+
add(addr, list);
78+
}
79+
return list;
80+
}
81+
82+
private void add(String addr, List<NetAddress> list) {
83+
NetAddress parse = NetAddress.parse(addr);
84+
if (parse != null) {
85+
list.add(parse);
86+
}
87+
}
88+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package top.meethigher.proxy.tcp.mux;
2+
3+
import io.vertx.core.Handler;
4+
import io.vertx.core.Vertx;
5+
import io.vertx.core.net.NetClient;
6+
import io.vertx.core.net.NetServer;
7+
import io.vertx.core.net.NetServerOptions;
8+
import io.vertx.core.net.NetSocket;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import top.meethigher.proxy.NetAddress;
12+
import top.meethigher.proxy.tcp.mux.model.MuxNetAddress;
13+
14+
import java.util.ArrayList;
15+
import java.util.HashMap;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.concurrent.ThreadLocalRandom;
19+
20+
/**
21+
* ReverseTcpProxyMuxClient
22+
* <p>
23+
* 本地启动多个端口,以{@code top.meethigher.proxy.tcp.mux.ReverseTcpProxyMuxServer }一个固定端口为内网的流量出入口,进而实现本地不通端口转发不同的内网服务功能
24+
*
25+
* @author <a href="https://meethigher.top">chenchuancheng</a>
26+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc1078">RFC 1078 - TCP port service Multiplexer (TCPMUX)</a>
27+
* @since 2025/07/26 20:43
28+
*/
29+
public class ReverseTcpProxyMuxClient extends Mux {
30+
31+
private static final Logger log = LoggerFactory.getLogger(ReverseTcpProxyMuxClient.class);
32+
/**
33+
* 端口映射关系
34+
* key: 本地监听的TCP服务
35+
* value: 经过 {@code top.meethigher.proxy.tcp.mux.ReverseTcpProxyMuxServer }转发的内网服务
36+
*/
37+
protected final Map<MuxNetAddress, List<NetAddress>> mapper;
38+
39+
protected final NetServerOptions netServerOptions;
40+
41+
protected final NetClient netClient;
42+
43+
protected final NetAddress muxServer;
44+
45+
protected final String name;
46+
47+
protected final List<NetServer> netServers = new ArrayList<>();
48+
49+
protected ReverseTcpProxyMuxClient(Vertx vertx, String secret, Map<MuxNetAddress, List<NetAddress>> mapper, NetServerOptions netServerOptions, NetClient netClient, NetAddress muxServer, String name) {
50+
super(vertx, secret);
51+
this.mapper = mapper;
52+
this.netServerOptions = netServerOptions;
53+
this.netClient = netClient;
54+
this.muxServer = muxServer;
55+
this.name = name;
56+
}
57+
58+
59+
protected void handleConnect(NetSocket src, MuxNetAddress local, List<NetAddress> backendServers) {
60+
src.pause();
61+
log.debug("{}: source {} -- {} connected", local.getName(), src.localAddress(), src.remoteAddress());
62+
src.closeHandler(v -> log.debug("{}: source {} -- {} closed", local.getName(), src.localAddress(), src.remoteAddress()));
63+
netClient.connect(muxServer.getPort(), muxServer.getHost())
64+
.onFailure(e -> {
65+
log.error("{}: failed to connect to {}", local.getName(), muxServer, e);
66+
src.close();
67+
})
68+
.onSuccess(dst -> {
69+
dst.pause();
70+
log.debug("{}: target {} -- {} connected", local.getName(), dst.localAddress(), dst.remoteAddress());
71+
dst.closeHandler(v -> log.debug("{}: target {} -- {} closed", local.getName(), dst.localAddress(), dst.remoteAddress()));
72+
Handler<Void> writeSuccessHandler = t -> {
73+
// https://github.com/meethigher/tcp-reverse-proxy/issues/12
74+
// 将日志记录详细,便于排查问题
75+
src.pipeTo(dst)
76+
.onSuccess(v -> log.debug("{}: source {} -- {} pipe to target {} -- {} succeeded",
77+
local.getName(), src.localAddress(), src.remoteAddress(), dst.localAddress(), dst.remoteAddress()))
78+
.onFailure(e -> log.error("{}: source {} -- {} pipe to target {} -- {} failed",
79+
local.getName(), src.localAddress(), src.remoteAddress(), dst.localAddress(), dst.remoteAddress(), e));
80+
dst.pipeTo(src)
81+
.onSuccess(v -> log.debug("{}: target {} -- {} pipe to source {} -- {} succeeded",
82+
local.getName(), dst.localAddress(), dst.remoteAddress(), src.localAddress(), src.remoteAddress()))
83+
.onFailure(e -> log.error("{}: target {} -- {} pipe to source {} -- {} failed",
84+
local.getName(), dst.localAddress(), dst.remoteAddress(), src.localAddress(), src.remoteAddress(), e));
85+
log.debug("{}: source {} -- {} bound to target {} -- {} with backend server {}",
86+
local.getName(),
87+
src.localAddress(), src.remoteAddress(),
88+
dst.localAddress(), dst.remoteAddress(),
89+
backendServers);
90+
src.resume();
91+
dst.resume();
92+
};
93+
dst.write(this.encode(backendServers))
94+
.onSuccess(writeSuccessHandler)
95+
.onFailure(e -> {
96+
dst.close();
97+
src.close();
98+
});
99+
});
100+
}
101+
102+
public void start() {
103+
for (MuxNetAddress local : mapper.keySet()) {
104+
vertx.createNetServer(netServerOptions)
105+
.connectHandler(src ->
106+
this.handleConnect(src, local, mapper.get(local)))
107+
.exceptionHandler(e -> log.error("{} {} socket errors happening before the connection is passed to the connectHandler", name, local.getName(), e))
108+
.listen(local.getPort(), local.getHost())
109+
.onFailure(e -> log.error("{} {} start failed on {}", name, local.getName(), local, e))
110+
.onSuccess(v -> {
111+
log.info("{} {} started on {}. mux server {}, backend server {}",
112+
name,
113+
local.getName(),
114+
local,
115+
muxServer,
116+
mapper.get(local)
117+
);
118+
netServers.add(v);
119+
});
120+
}
121+
}
122+
123+
public void stop() {
124+
for (NetServer netServer : netServers) {
125+
netServer.close()
126+
.onSuccess(v -> log.info("{} port {} closed", name, netServer.actualPort()))
127+
.onFailure(e -> log.error("{} port {} close failed", name, netServer.actualPort(), e));
128+
}
129+
netServers.clear();
130+
}
131+
132+
public static String generateName() {
133+
final String prefix = ReverseTcpProxyMuxClient.class.getSimpleName() + "-";
134+
try {
135+
// 池号对于虚拟机来说是全局的,以避免在类加载器范围的环境中池号重叠
136+
synchronized (System.getProperties()) {
137+
final String next = String.valueOf(Integer.getInteger(ReverseTcpProxyMuxClient.class.getName() + ".name", 0) + 1);
138+
System.setProperty(ReverseTcpProxyMuxClient.class.getName() + ".name", next);
139+
return prefix + next;
140+
}
141+
} catch (Exception e) {
142+
final ThreadLocalRandom random = ThreadLocalRandom.current();
143+
final StringBuilder sb = new StringBuilder(prefix);
144+
for (int i = 0; i < 4; i++) {
145+
sb.append(ID_CHARACTERS[random.nextInt(62)]);
146+
}
147+
return sb.toString();
148+
}
149+
}
150+
151+
public static ReverseTcpProxyMuxClient create() {
152+
Vertx vertx = Vertx.vertx();
153+
return new ReverseTcpProxyMuxClient(vertx, Mux.SECRET_DEFAULT,
154+
new HashMap<MuxNetAddress, List<NetAddress>>() {{
155+
put(new MuxNetAddress("0.0.0.0", 6666, "ssh1"),
156+
new ArrayList<NetAddress>() {{
157+
add(new NetAddress("127.0.0.1", 22));
158+
add(new NetAddress("127.0.0.1", 23));
159+
}});
160+
put(new MuxNetAddress("0.0.0.0", 6667, "ssh2"),
161+
new ArrayList<NetAddress>() {{
162+
add(new NetAddress("127.0.0.1", 22));
163+
add(new NetAddress("127.0.0.1", 23));
164+
}});
165+
166+
}}, new NetServerOptions(), vertx.createNetClient(),
167+
new NetAddress("10.0.0.30", 22),
168+
generateName());
169+
}
170+
171+
172+
}

0 commit comments

Comments
 (0)