From 6c4e4ca62e38dfdf5272cb69d4ae556bea15f976 Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Fri, 19 Jan 2024 20:00:09 +0800 Subject: [PATCH] refactor(ss): split the `AEADCipherCodec` into multiple parts --- .../ClientTCPChannelInitializer.java | 2 +- .../shadowsocks/ClientUDPReplayHandler.java | 2 +- .../codec/shadowsocks/AEADCipherCodec.java | 385 ------------------ .../codec/shadowsocks/AEADCipherCodecs.java | 47 --- .../common/codec/shadowsocks/Context.java | 20 - .../common/codec/shadowsocks/Keys.java | 20 + .../common/codec/shadowsocks/Session.java | 59 --- .../shadowsocks/tcp/AeadCipherCodec.java | 214 ++++++++++ .../shadowsocks/tcp/AeadCipherCodecs.java | 21 + .../common/codec/shadowsocks/tcp/Context.java | 8 + .../shadowsocks/{ => tcp}/TCPReplayCodec.java | 11 +- .../udp/Aead2022CipherCodecImpl.java | 180 ++++++++ .../shadowsocks/udp/AeadCipherCodec.java | 13 + .../shadowsocks/udp/AeadCipherCodecImpl.java | 49 +++ .../shadowsocks/udp/AeadCipherCodecs.java | 22 + .../common/codec/shadowsocks/udp/Context.java | 9 + .../shadowsocks/{ => udp}/UDPReplayCodec.java | 27 +- .../shadowsocks/aead2022/AEAD2022.java | 15 +- .../shadowsocks/aead2022}/Control.java | 16 +- .../shadowsocks/aead2022/Session.java | 46 +++ .../shadowsocks/aead2022}/UdpCipher.java | 2 +- ...DPCipherCache.java => UdpCipherCache.java} | 5 +- ...CipherCaches.java => UdpCipherCaches.java} | 9 +- .../src/com/urbanspork/server/Server.java | 39 +- .../urbanspork/server/ServerInitializer.java | 2 +- .../shadowsocks/AEADCipherCodecTestCase.java | 145 ------- .../shadowsocks/EmbeddedChannelTestCase.java | 2 + .../codec/shadowsocks/SessionTestCase.java | 43 -- .../tcp/AEADCipherCodecTestCase.java | 83 ++++ .../{ => tcp}/AEADCipherCodecsTestCase.java | 43 +- .../udp/AEADCipherCodecTestCase.java | 107 +++++ .../udp/AEADCipherCodecsTestCase.java | 107 +++++ ...022TestCase.java => Aead2022TestCase.java} | 12 +- .../shadowsocks/aead2022/ControlTestCase.java | 31 ++ .../shadowsocks/aead2022/SessionTestCase.java | 20 + .../aead2022/UDPAuthCacheTestCase.java | 5 +- .../aead2022/UDPCipherCacheTest.java | 10 +- .../test/com/urbanspork/test/TCPTestCase.java | 9 +- .../test/com/urbanspork/test/TestUtil.java | 9 +- .../test/com/urbanspork/test/UDPTestCase.java | 9 +- .../test/template/TCPTestTemplate.java | 12 +- .../test/template/TestTemplate.java | 9 + .../test/template/UDPTestTemplate.java | 7 + 43 files changed, 1069 insertions(+), 817 deletions(-) delete mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodec.java delete mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecs.java delete mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Context.java delete mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Session.java create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecs.java create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java rename urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/{ => tcp}/TCPReplayCodec.java (74%) create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecs.java create mode 100644 urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Context.java rename urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/{ => udp}/UDPReplayCodec.java (62%) rename urban-spork-common/src/com/urbanspork/common/{codec/shadowsocks => protocol/shadowsocks/aead2022}/Control.java (75%) create mode 100644 urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/Session.java rename urban-spork-common/src/com/urbanspork/common/{codec/shadowsocks => protocol/shadowsocks/aead2022}/UdpCipher.java (88%) rename urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/{UDPCipherCache.java => UdpCipherCache.java} (93%) rename urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/{UDPCipherCaches.java => UdpCipherCaches.java} (63%) delete mode 100644 urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecTestCase.java delete mode 100644 urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/SessionTestCase.java create mode 100644 urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AEADCipherCodecTestCase.java rename urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/{ => tcp}/AEADCipherCodecsTestCase.java (75%) create mode 100644 urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecTestCase.java create mode 100644 urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecsTestCase.java rename urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/{AEAD2022TestCase.java => Aead2022TestCase.java} (90%) create mode 100644 urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/ControlTestCase.java create mode 100644 urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/SessionTestCase.java diff --git a/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientTCPChannelInitializer.java b/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientTCPChannelInitializer.java index 8e089ef..0310cf2 100644 --- a/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientTCPChannelInitializer.java +++ b/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientTCPChannelInitializer.java @@ -1,7 +1,7 @@ package com.urbanspork.client.shadowsocks; import com.urbanspork.common.codec.shadowsocks.Mode; -import com.urbanspork.common.codec.shadowsocks.TCPReplayCodec; +import com.urbanspork.common.codec.shadowsocks.tcp.TCPReplayCodec; import com.urbanspork.common.config.ServerConfig; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; diff --git a/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUDPReplayHandler.java b/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUDPReplayHandler.java index b5d747f..9b2cc3a 100644 --- a/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUDPReplayHandler.java +++ b/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUDPReplayHandler.java @@ -3,7 +3,7 @@ import com.urbanspork.client.AbstractClientUDPReplayHandler; import com.urbanspork.common.channel.ExceptionHandler; import com.urbanspork.common.codec.shadowsocks.Mode; -import com.urbanspork.common.codec.shadowsocks.UDPReplayCodec; +import com.urbanspork.common.codec.shadowsocks.udp.UDPReplayCodec; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.protocol.network.TernaryDatagramPacket; import io.netty.bootstrap.Bootstrap; diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodec.java deleted file mode 100644 index ba17012..0000000 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodec.java +++ /dev/null @@ -1,385 +0,0 @@ -package com.urbanspork.common.codec.shadowsocks; - -import com.urbanspork.common.codec.CipherKind; -import com.urbanspork.common.codec.aead.Authenticator; -import com.urbanspork.common.codec.aead.CipherMethod; -import com.urbanspork.common.codec.aead.PayloadDecoder; -import com.urbanspork.common.codec.aead.PayloadEncoder; -import com.urbanspork.common.manage.shadowsocks.ServerUser; -import com.urbanspork.common.protocol.network.Network; -import com.urbanspork.common.protocol.shadowsocks.aead.AEAD; -import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; -import com.urbanspork.common.protocol.socks.Address; -import com.urbanspork.common.util.ByteString; -import com.urbanspork.common.util.Dice; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; -import org.bouncycastle.crypto.InvalidCipherTextException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; - -/** - * AEAD Cipher Codec - * - * @author Zmax0 - * @see AEAD ciphers - */ -class AEADCipherCodec { - - private static final Logger logger = LoggerFactory.getLogger(AEADCipherCodec.class); - private final Keys keys; - private final CipherKind cipherKind; - private final CipherMethod cipherMethod; - private PayloadEncoder payloadEncoder; - private PayloadDecoder payloadDecoder; - - AEADCipherCodec(CipherKind cipherKind, CipherMethod cipherMethod, Keys keys) { - this.keys = keys; - this.cipherKind = cipherKind; - this.cipherMethod = cipherMethod; - } - - public void encode(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { - boolean isAead2022 = cipherKind.isAead2022(); - if (context.network() == Network.UDP) { - encodePacket(context, isAead2022, msg, out); - } else { - if (payloadEncoder == null) { - initTcpPayloadEncoder(context, isAead2022, out); - logger.trace("[tcp][encode session]{}", context.session()); - if (Mode.Client == context.mode()) { - msg = handleRequestHeader(context, isAead2022, msg, out); - } else { - handleResponseHeader(context, isAead2022, msg, out); - } - } - payloadEncoder.encodePayload(msg, out); - } - } - - private void encodePacket(Context context, boolean isAead2022, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { - if (isAead2022) { - if (Mode.Client == context.mode()) { - encodeClientPacketAead2022(context, msg, out); - } else { - encodeServerPacketAead2022(context, msg, out); - } - } else { - Socks5CommandRequest request = context.request(); - byte[] salt = context.session().salt(); - out.writeBytes(salt); - ByteBuf temp = Unpooled.buffer(Address.getLength(request)); - Address.encode(request, temp); - AEAD.UDP.newPayloadEncoder(cipherMethod, keys.encKey(), salt).encodePacket(Unpooled.wrappedBuffer(temp, msg), out); - msg.skipBytes(msg.readableBytes()); - } - } - - // Client -> Server - private void encodeClientPacketAead2022(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { - Session session = context.session(); - logger.trace("[udp][encode session]{}", session); - Socks5CommandRequest request = context.request(); - int paddingLength = AEAD2022.getPaddingLength(msg); - int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); - int tagSize = cipherMethod.tagSize(); - byte[][] identityKeys = keys.identityKeys(); - boolean requireEih = cipherKind.supportEih() && identityKeys.length > 0; - int eihSize = requireEih ? identityKeys.length * 16 : 0; - ByteBuf temp = Unpooled.buffer(nonceLength + 8 + 8 + eihSize + 1 + 8 + 2 + paddingLength + Address.getLength(request) + msg.readableBytes() + tagSize); - // header fields - temp.writeLong(session.getClientSessionId()); - temp.writeLong(session.getPacketId()); - if (requireEih) { - byte[] sessionIdPacketId = new byte[16]; - temp.getBytes(nonceLength, sessionIdPacketId); - AEAD2022.UDP.withEih(keys.encKey(), identityKeys, sessionIdPacketId, temp); - } - temp.writeByte(Mode.Client.getValue()); - temp.writeLong(AEAD2022.newTimestamp()); - temp.writeShort(paddingLength); - temp.writeBytes(Dice.rollBytes(paddingLength)); - Address.encode(request, temp); - temp.writeBytes(msg); - UdpCipher cipher = AEAD2022.UDP.getCipher(cipherKind, cipherMethod, keys.encKey(), session.getClientSessionId()); - byte[] iPSK = identityKeys.length == 0 ? keys.encKey() : identityKeys[0]; - AEAD2022.UDP.encodePacket(cipher, iPSK, eihSize, temp, out); - } - - // Server -> Client - private void encodeServerPacketAead2022(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { - Session session = context.session(); - Control control = context.control(); - logger.trace("[udp][encode control]{}", control); - int paddingLength = AEAD2022.getPaddingLength(msg); - int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); - Socks5CommandRequest request = context.request(); - ByteBuf temp = Unpooled.buffer(nonceLength + 8 + 8 + 1 + 8 + 8 + 2 + paddingLength + Address.getLength(request) + msg.readableBytes() + cipherMethod.tagSize()); - // header fields - temp.writeLong(session.getServerSessionId()); - temp.writeLong(control.getPacketId()); - temp.writeByte(Mode.Server.getValue()); - temp.writeLong(AEAD2022.newTimestamp()); - temp.writeLong(control.getClientSessionId()); - temp.writeShort(paddingLength); - temp.writeBytes(Dice.rollBytes(paddingLength)); - Address.encode(request, temp); - temp.writeBytes(msg); - ServerUser user = context.session().getUser(); - byte[] key; - if (user != null) { - key = user.key(); - logger.trace("udp encrypt with {} identity", user); - } else { - key = keys.encKey(); - } - UdpCipher cipher = AEAD2022.UDP.getCipher(cipherKind, cipherMethod, key, session.getServerSessionId()); - AEAD2022.UDP.encodePacket(cipher, key, 0, temp, out); - } - - private void initTcpPayloadEncoder(Context context, boolean isAead2022, ByteBuf out) { - withIdentity(context, cipherKind, keys, out); - byte[] salt = context.session().salt(); - if (isAead2022) { - ServerUser user = context.session().getUser(); - if (user != null) { - payloadEncoder = AEAD2022.TCP.newPayloadEncoder(cipherMethod, user.key(), salt); - } else { - payloadEncoder = AEAD2022.TCP.newPayloadEncoder(cipherMethod, keys.encKey(), salt); - } - } else { - payloadEncoder = AEAD.TCP.newPayloadEncoder(cipherMethod, keys.encKey(), salt); - } - } - - private ByteBuf handleRequestHeader(Context context, boolean isAead2022, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { - ByteBuf temp = Unpooled.buffer(); - Address.encode(context.request(), temp); - if (isAead2022) { - int paddingLength = AEAD2022.getPaddingLength(msg); - temp.writeShort(paddingLength); - temp.writeBytes(Dice.rollBytes(paddingLength)); - } - temp = Unpooled.wrappedBuffer(temp, msg); - msg.skipBytes(msg.readableBytes()); - if (isAead2022) { - for (byte[] bytes : AEAD2022.TCP.newHeader(context.mode(), context.session().getRequestSalt(), temp)) { - out.writeBytes(payloadEncoder.auth().seal(bytes)); - } - } - return temp; - } - - private void handleResponseHeader(Context context, boolean isAead2022, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { - if (isAead2022) { - for (byte[] bytes : AEAD2022.TCP.newHeader(context.mode(), context.session().getRequestSalt(), msg)) { - out.writeBytes(payloadEncoder.auth().seal(bytes)); - } - } - } - - public void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException { - if (context.network() == Network.UDP) { - decodePacket(context, cipherKind.isAead2022(), in, out); - } else { - if (payloadDecoder == null) { - initPayloadDecoder(context, cipherKind, in, out); - if (payloadDecoder == null) { - return; - } - logger.trace("[tcp][decode session]{}", context.session()); - } - payloadDecoder.decodePayload(in, out); - } - } - - private void decodePacket(Context context, boolean isAead2022, ByteBuf in, List out) throws InvalidCipherTextException { - if (isAead2022) { - if (Mode.Client == context.mode()) { - decodeServerPocketAead2022(context, in, out); - } else { - decodeClientPocketAead2022(context, in, out); - } - } else { - byte[] salt = context.session().salt(); - in.readBytes(salt); - ByteBuf packet = AEAD.UDP.newPayloadDecoder(cipherMethod, keys.encKey(), salt).decodePacket(in); - Address.decode(packet, out); - out.add(packet.slice()); - } - } - - // Client -> Server - private void decodeClientPocketAead2022(Context context, ByteBuf in, List out) throws InvalidCipherTextException { - int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); - int tagSize = cipherMethod.tagSize(); - boolean requireEih = cipherKind.supportEih() && context.userManager().userCount() > 0; - int eihSize = requireEih ? 16 : 0; - int headerLength = nonceLength + tagSize + 8 + 8 + eihSize + 1 + 8 + 2; - if (headerLength > in.readableBytes()) { - String msg = String.format("packet too short, at least %d bytes, but found %d bytes", headerLength, in.readableBytes()); - throw new DecoderException(msg); - } - ByteBuf packet = AEAD2022.UDP.decodePacket(cipherKind, cipherMethod, context, keys.encKey(), in); - long clientSessionId = packet.readLong(); - long packetId = packet.readLong(); - if (requireEih) { - packet.skipBytes(16); - } - byte socketType = packet.readByte(); - if (Mode.Client.getValue() != socketType) { - String msg = String.format("invalid socket type, expecting %d, but found %d", Mode.Client.getValue(), socketType); - throw new DecoderException(msg); - } - AEAD2022.validateTimestamp(packet.readLong()); - int paddingLength = packet.readUnsignedShort(); - if (paddingLength > 0) { - packet.skipBytes(paddingLength); - } - Control control = context.control(); - control.setClientSessionId(clientSessionId); - control.setServerSessionId(0); - control.setPacketId(packetId); - logger.trace("[udp][decode control]{}", control); - Address.decode(packet, out); - out.add(packet.slice()); - } - - // Server -> Client - private void decodeServerPocketAead2022(Context context, ByteBuf in, List out) throws InvalidCipherTextException { - int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); - int tagSize = cipherMethod.tagSize(); - int headerLength = nonceLength + tagSize + 8 + 8 + 1 + 8 + 2; - if (headerLength > in.readableBytes()) { - String msg = String.format("packet too short, at least %d bytes, but found %d bytes", headerLength, in.readableBytes()); - throw new DecoderException(msg); - } - ByteBuf packet = AEAD2022.UDP.decodePacket(cipherKind, cipherMethod, context, keys.encKey(), in); - long serverSessionId = packet.readLong(); - long packetId = packet.readLong(); - byte socketType = packet.readByte(); - if (Mode.Server.getValue() != socketType) { - String msg = String.format("invalid socket type, expecting %d, but found %d", Mode.Server.getValue(), socketType); - throw new DecoderException(msg); - } - AEAD2022.validateTimestamp(packet.readLong()); - long clientSessionId = packet.readLong(); - int paddingLength = packet.readUnsignedShort(); - if (paddingLength > 0) { - packet.skipBytes(paddingLength); - } - Control control = context.control(); - control.setClientSessionId(clientSessionId); - control.setServerSessionId(serverSessionId); - control.setPacketId(packetId); - Address.decode(packet, out); - out.add(packet.slice()); - } - - private void initPayloadDecoder(Context context, CipherKind kind, ByteBuf in, List out) throws InvalidCipherTextException { - if (in.readableBytes() < context.session().salt().length) { - return; - } - in.markReaderIndex(); - if (kind.isAead2022()) { - initAEAD2022PayloadDecoder(context, in, out); - } else { - initPayloadDecoder(context, in, out); - } - } - - private void initAEAD2022PayloadDecoder(Context context, ByteBuf in, List out) throws InvalidCipherTextException { - int tagSize = cipherMethod.tagSize(); - int saltLength = cipherKind.keySize(); - int requestSaltLength = Mode.Server == context.mode() ? 0 : saltLength; - boolean requireEih = Mode.Server == context.mode() && cipherKind.supportEih() && context.userManager().userCount() > 0; - int eihLength = requireEih ? 16 : 0; - byte[] salt = new byte[saltLength]; - in.readBytes(salt); - if (logger.isTraceEnabled()) { - logger.trace("get AEAD salt {}", ByteString.valueOf(salt)); - } - context.session().setRequestSalt(salt); - ByteBuf sealedHeaderBuf = in.readBytes(eihLength + 1 + 8 + requestSaltLength + 2 + tagSize); - PayloadDecoder newPayloadDecoder; - if (requireEih) { - if (sealedHeaderBuf.readableBytes() < 16) { - String msg = String.format("expecting EIH, but header chunk len: %d", sealedHeaderBuf.readableBytes()); - throw new DecoderException(msg); - } - byte[] eih = new byte[16]; - sealedHeaderBuf.readBytes(eih); - newPayloadDecoder = AEAD2022.TCP.newPayloadDecoder(cipherMethod, context, keys.encKey(), salt, eih); - } else { - newPayloadDecoder = AEAD2022.TCP.newPayloadDecoder(cipherMethod, keys.encKey(), salt); - } - Authenticator auth = newPayloadDecoder.auth(); - byte[] sealedHeaderBytes = new byte[sealedHeaderBuf.readableBytes()]; - sealedHeaderBuf.readBytes(sealedHeaderBytes); - sealedHeaderBuf.release(); - ByteBuf headerBuf = Unpooled.wrappedBuffer(auth.open(sealedHeaderBytes)); - byte streamTypeByte = headerBuf.readByte(); - Mode expectedMode = switch (context.mode()) { - case Client -> Mode.Server; - case Server -> Mode.Client; - }; - byte expectedStreamTypeByte = expectedMode.getValue(); - if (expectedStreamTypeByte != streamTypeByte) { - String msg = String.format("invalid stream type, expecting %d, but found %d", expectedStreamTypeByte, streamTypeByte); - throw new DecoderException(msg); - } - AEAD2022.validateTimestamp(headerBuf.readLong()); - if (Mode.Client == context.mode()) { - byte[] requestSalt = new byte[salt.length]; - headerBuf.readBytes(requestSalt); - context.session().setRequestSalt(requestSalt); - } - int length = headerBuf.readUnsignedShort(); - if (in.readableBytes() < length + tagSize) { - in.resetReaderIndex(); - return; - } - byte[] encryptedPayloadBytes = new byte[length + tagSize]; - in.readBytes(encryptedPayloadBytes); - ByteBuf first = Unpooled.wrappedBuffer(auth.open(encryptedPayloadBytes)); - if (Mode.Server == context.mode()) { - Address.decode(first, out); - int paddingLength = first.readUnsignedShort(); - first.skipBytes(paddingLength); - out.add(first); - } else { - out.add(first); - } - this.payloadDecoder = newPayloadDecoder; - } - - private void initPayloadDecoder(Context context, ByteBuf in, List out) throws InvalidCipherTextException { - byte[] salt = context.session().salt(); - in.readBytes(salt); - PayloadDecoder newPayloadDecoder = AEAD.TCP.newPayloadDecoder(cipherMethod, keys.encKey(), salt); - List list = new ArrayList<>(1); - newPayloadDecoder.decodePayload(in, list); - if (list.isEmpty()) { - in.resetReaderIndex(); - return; - } - if (Mode.Server == context.mode()) { - Address.decode((ByteBuf) list.getFirst(), out); - } - out.addAll(list); - this.payloadDecoder = newPayloadDecoder; - } - - static void withIdentity(Context context, CipherKind kind, Keys keys, ByteBuf out) { - byte[] salt = context.session().salt(); - out.writeBytes(salt); // salt should be sent with the first chunk - if (Mode.Client == context.mode() && kind.supportEih()) { - AEAD2022.TCP.withEih(keys, salt, out); - } - } -} \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecs.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecs.java deleted file mode 100644 index a545407..0000000 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecs.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.urbanspork.common.codec.shadowsocks; - -import com.urbanspork.common.codec.CipherKind; -import com.urbanspork.common.codec.aead.CipherMethods; -import com.urbanspork.common.config.ServerConfig; -import com.urbanspork.common.config.ServerUserConfig; -import com.urbanspork.common.manage.shadowsocks.ServerUser; -import com.urbanspork.common.manage.shadowsocks.ServerUserManager; -import com.urbanspork.common.protocol.shadowsocks.aead.AEAD; -import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; - -import java.util.List; - -public class AEADCipherCodecs { - - private AEADCipherCodecs() {} - - static AEADCipherCodec get(ServerConfig config) { - List user = config.getUser(); - if (user != null) { - user.stream().map(ServerUser::from).forEach(ServerUserManager.DEFAULT::addUser); - } - CipherKind kind = config.getCipher(); - Keys keys = passwordToKeys(kind, config.getPassword()); - if (CipherKind.chacha20_poly1305 == kind) { - return new AEADCipherCodec(kind, CipherMethods.CHACHA20_POLY1305.get(), keys); - } else { - return new AEADCipherCodec(kind, CipherMethods.AES_GCM.get(), keys); - } - } - - static Keys passwordToKeys(CipherKind kind, String password) { - int keySize = kind.keySize(); - Keys keys; - if (kind.isAead2022()) { - keys = AEAD2022.passwordToKeys(password); - } else { - keys = new Keys(AEAD.opensslBytesToKey(password.getBytes(), keySize), new byte[][]{}); - } - if (keys.encKey().length != keySize) { - String msg = String.format("%s is expecting a %d bytes key, but password: %s (%d bytes after decode)", - kind, keySize, password, keys.encKey().length); - throw new IllegalArgumentException(msg); - } - return keys; - } -} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Context.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Context.java deleted file mode 100644 index d5f9401..0000000 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Context.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.urbanspork.common.codec.shadowsocks; - -import com.urbanspork.common.codec.CipherKind; -import com.urbanspork.common.manage.shadowsocks.ServerUserManager; -import com.urbanspork.common.protocol.network.Network; -import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; - -public record Context(Network network, Mode mode, Session session, Control control, Socks5CommandRequest request, ServerUserManager userManager) { - Context(Network network, Mode mode, CipherKind kind, Socks5CommandRequest request, ServerUserManager userManager) { - this(network, mode, getSession(network, kind), new Control(), request, userManager); - } - - private static Session getSession(Network network, CipherKind kind) { - if (Network.UDP == network) { - return Session.udp(kind); - } else { - return Session.tcp(kind); - } - } -} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Keys.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Keys.java index 37a52d4..751c042 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Keys.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Keys.java @@ -1,10 +1,30 @@ package com.urbanspork.common.codec.shadowsocks; +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.protocol.shadowsocks.aead.AEAD; +import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; + import java.util.Base64; import java.util.stream.Collectors; import java.util.stream.Stream; public record Keys(byte[] encKey, byte[][] identityKeys) { + public static Keys from(CipherKind kind, String password) { + int keySize = kind.keySize(); + Keys keys; + if (kind.isAead2022()) { + keys = AEAD2022.passwordToKeys(password); + } else { + keys = new Keys(AEAD.opensslBytesToKey(password.getBytes(), keySize), new byte[][]{}); + } + if (keys.encKey().length != keySize) { + String msg = String.format("%s is expecting a %d bytes key, but password: %s (%d bytes after decode)", + kind, keySize, password, keys.encKey().length); + throw new IllegalArgumentException(msg); + } + return keys; + } + @Override public String toString() { return String.format("EK:%s, IK:%s", diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Session.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Session.java deleted file mode 100644 index d4c9706..0000000 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Session.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.urbanspork.common.codec.shadowsocks; - -import com.urbanspork.common.codec.CipherKind; -import com.urbanspork.common.protocol.network.Network; -import com.urbanspork.common.util.ByteString; -import com.urbanspork.common.util.Dice; - -import java.util.concurrent.ThreadLocalRandom; - -public class Session extends Control { - - private final Network network; - private final byte[] salt; - private byte[] requestSalt; - - Session(Network network, long packetId, long clientSessionId, long serverSessionId, byte[] salt, byte[] requestSalt) { - super(clientSessionId, serverSessionId, packetId, null); - this.network = network; - this.salt = salt; - this.requestSalt = requestSalt; - } - - public static Session tcp(CipherKind kind) { - int length = kind.keySize(); - byte[] salt = Dice.rollBytes(length); - return new Session(Network.TCP, 0, 0, 0, salt, null); - } - - public static Session udp(CipherKind kind) { - int length = kind.keySize(); - byte[] salt = Dice.rollBytes(length); - long clientSessionId = ThreadLocalRandom.current().nextLong(); - long serverSessionId = ThreadLocalRandom.current().nextLong(); - return new Session(Network.UDP, 1, clientSessionId, serverSessionId, salt, null); - } - - public byte[] salt() { - return salt; - } - - public byte[] getRequestSalt() { - return requestSalt; - } - - - public void setRequestSalt(byte[] requestSalt) { - this.requestSalt = requestSalt; - } - - - @Override - public String toString() { - if (Network.UDP == network) { - return super.toString(); - } else { - return String.format("S:%s, RS:%s", ByteString.valueOf(salt), ByteString.valueOf(requestSalt)); - } - } -} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java new file mode 100644 index 0000000..7fef6f3 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java @@ -0,0 +1,214 @@ +package com.urbanspork.common.codec.shadowsocks.tcp; + +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.aead.Authenticator; +import com.urbanspork.common.codec.aead.CipherMethod; +import com.urbanspork.common.codec.aead.PayloadDecoder; +import com.urbanspork.common.codec.aead.PayloadEncoder; +import com.urbanspork.common.codec.shadowsocks.Keys; +import com.urbanspork.common.codec.shadowsocks.Mode; +import com.urbanspork.common.manage.shadowsocks.ServerUser; +import com.urbanspork.common.protocol.shadowsocks.aead.AEAD; +import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; +import com.urbanspork.common.protocol.socks.Address; +import com.urbanspork.common.util.ByteString; +import com.urbanspork.common.util.Dice; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DecoderException; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * AEAD Cipher Codec + * + * @author Zmax0 + * @see AEAD ciphers + */ +class AeadCipherCodec { + + private static final Logger logger = LoggerFactory.getLogger(AeadCipherCodec.class); + private final Keys keys; + private final CipherKind cipherKind; + private final CipherMethod cipherMethod; + private PayloadEncoder payloadEncoder; + private PayloadDecoder payloadDecoder; + + AeadCipherCodec(CipherKind cipherKind, CipherMethod cipherMethod, Keys keys) { + this.keys = keys; + this.cipherKind = cipherKind; + this.cipherMethod = cipherMethod; + } + + public void encode(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { + boolean isAead2022 = cipherKind.isAead2022(); + if (payloadEncoder == null) { + initTcpPayloadEncoder(context, isAead2022, out); + logger.trace("[tcp][encode session]{}", context.session()); + if (Mode.Client == context.mode()) { + msg = handleRequestHeader(context, isAead2022, msg, out); + } else { + handleResponseHeader(context, isAead2022, msg, out); + } + } + payloadEncoder.encodePayload(msg, out); + } + + private void initTcpPayloadEncoder(Context context, boolean isAead2022, ByteBuf out) { + withIdentity(context, cipherKind, keys, out); + byte[] salt = context.session().salt(); + if (isAead2022) { + ServerUser user = context.session().getUser(); + if (user != null) { + payloadEncoder = AEAD2022.TCP.newPayloadEncoder(cipherMethod, user.key(), salt); + } else { + payloadEncoder = AEAD2022.TCP.newPayloadEncoder(cipherMethod, keys.encKey(), salt); + } + } else { + payloadEncoder = AEAD.TCP.newPayloadEncoder(cipherMethod, keys.encKey(), salt); + } + } + + private ByteBuf handleRequestHeader(Context context, boolean isAead2022, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { + ByteBuf temp = Unpooled.buffer(); + Address.encode(context.request(), temp); + if (isAead2022) { + int paddingLength = AEAD2022.getPaddingLength(msg); + temp.writeShort(paddingLength); + temp.writeBytes(Dice.rollBytes(paddingLength)); + } + temp = Unpooled.wrappedBuffer(temp, msg); + msg.skipBytes(msg.readableBytes()); + if (isAead2022) { + for (byte[] bytes : AEAD2022.TCP.newHeader(context.mode(), context.session().getRequestSalt(), temp)) { + out.writeBytes(payloadEncoder.auth().seal(bytes)); + } + } + return temp; + } + + private void handleResponseHeader(Context context, boolean isAead2022, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { + if (isAead2022) { + for (byte[] bytes : AEAD2022.TCP.newHeader(context.mode(), context.session().getRequestSalt(), msg)) { + out.writeBytes(payloadEncoder.auth().seal(bytes)); + } + } + } + + public void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + if (payloadDecoder == null) { + initPayloadDecoder(context, cipherKind, in, out); + if (payloadDecoder == null) { + return; + } + logger.trace("[tcp][decode session]{}", context.session()); + } + payloadDecoder.decodePayload(in, out); + } + + private void initPayloadDecoder(Context context, CipherKind kind, ByteBuf in, List out) throws InvalidCipherTextException { + if (in.readableBytes() < context.session().salt().length) { + return; + } + in.markReaderIndex(); + if (kind.isAead2022()) { + initAEAD2022PayloadDecoder(context, in, out); + } else { + initPayloadDecoder(context, in, out); + } + } + + private void initAEAD2022PayloadDecoder(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + int tagSize = cipherMethod.tagSize(); + int saltLength = cipherKind.keySize(); + int requestSaltLength = Mode.Server == context.mode() ? 0 : saltLength; + boolean requireEih = Mode.Server == context.mode() && cipherKind.supportEih() && context.userManager().userCount() > 0; + int eihLength = requireEih ? 16 : 0; + byte[] salt = new byte[saltLength]; + in.readBytes(salt); + if (logger.isTraceEnabled()) { + logger.trace("get AEAD salt {}", ByteString.valueOf(salt)); + } + context.session().setRequestSalt(salt); + ByteBuf sealedHeaderBuf = in.readBytes(eihLength + 1 + 8 + requestSaltLength + 2 + tagSize); + PayloadDecoder newPayloadDecoder; + if (requireEih) { + if (sealedHeaderBuf.readableBytes() < 16) { + String msg = String.format("expecting EIH, but header chunk len: %d", sealedHeaderBuf.readableBytes()); + throw new DecoderException(msg); + } + byte[] eih = new byte[16]; + sealedHeaderBuf.readBytes(eih); + newPayloadDecoder = AEAD2022.TCP.newPayloadDecoder(cipherMethod, context.session(), context.userManager(), keys.encKey(), salt, eih); + } else { + newPayloadDecoder = AEAD2022.TCP.newPayloadDecoder(cipherMethod, keys.encKey(), salt); + } + Authenticator auth = newPayloadDecoder.auth(); + byte[] sealedHeaderBytes = new byte[sealedHeaderBuf.readableBytes()]; + sealedHeaderBuf.readBytes(sealedHeaderBytes); + sealedHeaderBuf.release(); + ByteBuf headerBuf = Unpooled.wrappedBuffer(auth.open(sealedHeaderBytes)); + byte streamTypeByte = headerBuf.readByte(); + Mode expectedMode = switch (context.mode()) { + case Client -> Mode.Server; + case Server -> Mode.Client; + }; + byte expectedStreamTypeByte = expectedMode.getValue(); + if (expectedStreamTypeByte != streamTypeByte) { + String msg = String.format("invalid stream type, expecting %d, but found %d", expectedStreamTypeByte, streamTypeByte); + throw new DecoderException(msg); + } + AEAD2022.validateTimestamp(headerBuf.readLong()); + if (Mode.Client == context.mode()) { + byte[] requestSalt = new byte[salt.length]; + headerBuf.readBytes(requestSalt); + context.session().setRequestSalt(requestSalt); + } + int length = headerBuf.readUnsignedShort(); + if (in.readableBytes() < length + tagSize) { + in.resetReaderIndex(); + return; + } + byte[] encryptedPayloadBytes = new byte[length + tagSize]; + in.readBytes(encryptedPayloadBytes); + ByteBuf first = Unpooled.wrappedBuffer(auth.open(encryptedPayloadBytes)); + if (Mode.Server == context.mode()) { + Address.decode(first, out); + int paddingLength = first.readUnsignedShort(); + first.skipBytes(paddingLength); + out.add(first); + } else { + out.add(first); + } + this.payloadDecoder = newPayloadDecoder; + } + + private void initPayloadDecoder(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + byte[] salt = context.session().salt(); + in.readBytes(salt); + PayloadDecoder newPayloadDecoder = AEAD.TCP.newPayloadDecoder(cipherMethod, keys.encKey(), salt); + List list = new ArrayList<>(1); + newPayloadDecoder.decodePayload(in, list); + if (list.isEmpty()) { + in.resetReaderIndex(); + return; + } + if (Mode.Server == context.mode()) { + Address.decode((ByteBuf) list.getFirst(), out); + } + out.addAll(list); + this.payloadDecoder = newPayloadDecoder; + } + + static void withIdentity(Context context, CipherKind kind, Keys keys, ByteBuf out) { + byte[] salt = context.session().salt(); + out.writeBytes(salt); // salt should be sent with the first chunk + if (Mode.Client == context.mode() && kind.supportEih()) { + AEAD2022.TCP.withEih(keys, salt, out); + } + } +} \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecs.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecs.java new file mode 100644 index 0000000..374d42c --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecs.java @@ -0,0 +1,21 @@ +package com.urbanspork.common.codec.shadowsocks.tcp; + +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.aead.CipherMethods; +import com.urbanspork.common.codec.shadowsocks.Keys; +import com.urbanspork.common.config.ServerConfig; + +class AeadCipherCodecs { + + private AeadCipherCodecs() {} + + static AeadCipherCodec get(ServerConfig config) { + CipherKind kind = config.getCipher(); + Keys keys = Keys.from(kind, config.getPassword()); + if (CipherKind.chacha20_poly1305 == kind) { + return new AeadCipherCodec(kind, CipherMethods.CHACHA20_POLY1305.get(), keys); + } else { + return new AeadCipherCodec(kind, CipherMethods.AES_GCM.get(), keys); + } + } +} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java new file mode 100644 index 0000000..edcb5da --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java @@ -0,0 +1,8 @@ +package com.urbanspork.common.codec.shadowsocks.tcp; + +import com.urbanspork.common.codec.shadowsocks.Mode; +import com.urbanspork.common.manage.shadowsocks.ServerUserManager; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Session; +import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; + +record Context(Mode mode, Session session, Socks5CommandRequest request, ServerUserManager userManager) {} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/TCPReplayCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/TCPReplayCodec.java similarity index 74% rename from urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/TCPReplayCodec.java rename to urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/TCPReplayCodec.java index 277f23b..3687706 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/TCPReplayCodec.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/TCPReplayCodec.java @@ -1,8 +1,9 @@ -package com.urbanspork.common.codec.shadowsocks; +package com.urbanspork.common.codec.shadowsocks.tcp; +import com.urbanspork.common.codec.shadowsocks.Mode; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.manage.shadowsocks.ServerUserManager; -import com.urbanspork.common.protocol.network.Network; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Session; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageCodec; @@ -13,7 +14,7 @@ public class TCPReplayCodec extends ByteToMessageCodec { private final Context context; - private final AEADCipherCodec cipher; + private final AeadCipherCodec cipher; public TCPReplayCodec(Mode mode, ServerConfig config) { this(mode, null, config); @@ -21,8 +22,8 @@ public TCPReplayCodec(Mode mode, ServerConfig config) { public TCPReplayCodec(Mode mode, Socks5CommandRequest request, ServerConfig config) { ServerUserManager userManager = Mode.Server == mode ? ServerUserManager.DEFAULT : ServerUserManager.EMPTY; - this.context = new Context(Network.TCP, mode, Session.tcp(config.getCipher()), new Control(), request, userManager); - this.cipher = AEADCipherCodecs.get(config); + this.context = new Context(mode, new Session(config.getCipher()), request, userManager); + this.cipher = AeadCipherCodecs.get(config); } @Override diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java new file mode 100644 index 0000000..cc0dfb4 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java @@ -0,0 +1,180 @@ +package com.urbanspork.common.codec.shadowsocks.udp; + +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.aead.CipherMethod; +import com.urbanspork.common.codec.shadowsocks.Keys; +import com.urbanspork.common.codec.shadowsocks.Mode; +import com.urbanspork.common.manage.shadowsocks.ServerUser; +import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Control; +import com.urbanspork.common.protocol.shadowsocks.aead2022.UdpCipher; +import com.urbanspork.common.protocol.socks.Address; +import com.urbanspork.common.util.Dice; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DecoderException; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.util.List; + +class Aead2022CipherCodecImpl implements AeadCipherCodec { + private static final Logger logger = LoggerFactory.getLogger(Aead2022CipherCodecImpl.class); + private final Keys keys; + private final CipherKind cipherKind; + private final CipherMethod cipherMethod; + + Aead2022CipherCodecImpl(CipherKind cipherKind, CipherMethod cipherMethod, Keys keys) { + this.keys = keys; + this.cipherKind = cipherKind; + this.cipherMethod = cipherMethod; + } + + @Override + public void encode(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { + if (Mode.Client == context.mode()) { + encodeClientPacketAead2022(context, msg, out); + } else { + encodeServerPacketAead2022(context, msg, out); + } + } + + // Client -> Server + private void encodeClientPacketAead2022(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { + Control control = context.control(); + logger.trace("[udp][encode control]{}", control); + InetSocketAddress address = context.address(); + int paddingLength = AEAD2022.getPaddingLength(msg); + int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); + int tagSize = cipherMethod.tagSize(); + byte[][] identityKeys = keys.identityKeys(); + boolean requireEih = cipherKind.supportEih() && identityKeys.length > 0; + int eihSize = requireEih ? identityKeys.length * 16 : 0; + ByteBuf temp = Unpooled.buffer(nonceLength + 8 + 8 + eihSize + 1 + 8 + 2 + paddingLength + Address.getLength(address) + msg.readableBytes() + tagSize); + // header fields + temp.writeLong(control.getClientSessionId()); + temp.writeLong(control.getPacketId()); + if (requireEih) { + byte[] sessionIdPacketId = new byte[16]; + temp.getBytes(nonceLength, sessionIdPacketId); + AEAD2022.UDP.withEih(keys.encKey(), identityKeys, sessionIdPacketId, temp); + } + temp.writeByte(Mode.Client.getValue()); + temp.writeLong(AEAD2022.newTimestamp()); + temp.writeShort(paddingLength); + temp.writeBytes(Dice.rollBytes(paddingLength)); + Address.encode(address, temp); + temp.writeBytes(msg); + UdpCipher cipher = AEAD2022.UDP.getCipher(cipherKind, cipherMethod, keys.encKey(), control.getClientSessionId()); + byte[] iPSK = identityKeys.length == 0 ? keys.encKey() : identityKeys[0]; + AEAD2022.UDP.encodePacket(cipher, iPSK, eihSize, temp, out); + } + + // Server -> Client + private void encodeServerPacketAead2022(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { + Control control = context.control(); + logger.trace("[udp][encode control]{}", control); + int paddingLength = AEAD2022.getPaddingLength(msg); + int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); + InetSocketAddress address = context.address(); + ByteBuf temp = Unpooled.buffer(nonceLength + 8 + 8 + 1 + 8 + 8 + 2 + paddingLength + Address.getLength(address) + msg.readableBytes() + cipherMethod.tagSize()); + // header fields + temp.writeLong(control.getServerSessionId()); + temp.writeLong(control.getPacketId()); + temp.writeByte(Mode.Server.getValue()); + temp.writeLong(AEAD2022.newTimestamp()); + temp.writeLong(control.getClientSessionId()); + temp.writeShort(paddingLength); + temp.writeBytes(Dice.rollBytes(paddingLength)); + Address.encode(address, temp); + temp.writeBytes(msg); + ServerUser user = control.getUser(); + byte[] key; + if (user != null) { + key = user.key(); + logger.trace("udp encrypt with {} identity", user); + } else { + key = keys.encKey(); + } + UdpCipher cipher = AEAD2022.UDP.getCipher(cipherKind, cipherMethod, key, control.getServerSessionId()); + AEAD2022.UDP.encodePacket(cipher, key, 0, temp, out); + } + + @Override + public void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + if (Mode.Client == context.mode()) { + decodeServerPocketAead2022(context, in, out); + } else { + decodeClientPocketAead2022(context, in, out); + } + } + + // Client -> Server + private void decodeClientPocketAead2022(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); + int tagSize = cipherMethod.tagSize(); + boolean requireEih = cipherKind.supportEih() && context.userManager().userCount() > 0; + int eihSize = requireEih ? 16 : 0; + int headerLength = nonceLength + tagSize + 8 + 8 + eihSize + 1 + 8 + 2; + if (headerLength > in.readableBytes()) { + String msg = String.format("packet too short, at least %d bytes, but found %d bytes", headerLength, in.readableBytes()); + throw new DecoderException(msg); + } + ByteBuf packet = AEAD2022.UDP.decodePacket(cipherKind, cipherMethod, context.control(), context.userManager(), keys.encKey(), in); + long clientSessionId = packet.readLong(); + long packetId = packet.readLong(); + if (requireEih) { + packet.skipBytes(16); + } + byte socketType = packet.readByte(); + if (Mode.Client.getValue() != socketType) { + String msg = String.format("invalid socket type, expecting %d, but found %d", Mode.Client.getValue(), socketType); + throw new DecoderException(msg); + } + AEAD2022.validateTimestamp(packet.readLong()); + int paddingLength = packet.readUnsignedShort(); + if (paddingLength > 0) { + packet.skipBytes(paddingLength); + } + Control control = context.control(); + control.setClientSessionId(clientSessionId); + control.setServerSessionId(0); + control.setPacketId(packetId); + logger.trace("[udp][decode control]{}", control); + Address.decode(packet, out); + out.add(packet.slice()); + } + + // Server -> Client + private void decodeServerPocketAead2022(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); + int tagSize = cipherMethod.tagSize(); + int headerLength = nonceLength + tagSize + 8 + 8 + 1 + 8 + 2; + if (headerLength > in.readableBytes()) { + String msg = String.format("packet too short, at least %d bytes, but found %d bytes", headerLength, in.readableBytes()); + throw new DecoderException(msg); + } + ByteBuf packet = AEAD2022.UDP.decodePacket(cipherKind, cipherMethod, context.control(), context.userManager(), keys.encKey(), in); + long serverSessionId = packet.readLong(); + long packetId = packet.readLong(); + byte socketType = packet.readByte(); + if (Mode.Server.getValue() != socketType) { + String msg = String.format("invalid socket type, expecting %d, but found %d", Mode.Server.getValue(), socketType); + throw new DecoderException(msg); + } + AEAD2022.validateTimestamp(packet.readLong()); + long clientSessionId = packet.readLong(); + int paddingLength = packet.readUnsignedShort(); + if (paddingLength > 0) { + packet.skipBytes(paddingLength); + } + Control control = context.control(); + control.setClientSessionId(clientSessionId); + control.setServerSessionId(serverSessionId); + control.setPacketId(packetId); + Address.decode(packet, out); + out.add(packet.slice()); + } +} \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java new file mode 100644 index 0000000..ae314b1 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java @@ -0,0 +1,13 @@ +package com.urbanspork.common.codec.shadowsocks.udp; + +import io.netty.buffer.ByteBuf; +import org.bouncycastle.crypto.InvalidCipherTextException; + +import java.util.List; + +interface AeadCipherCodec { + + void encode(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException; + + void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException; +} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java new file mode 100644 index 0000000..785d740 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java @@ -0,0 +1,49 @@ +package com.urbanspork.common.codec.shadowsocks.udp; + +import com.urbanspork.common.codec.aead.CipherMethod; +import com.urbanspork.common.codec.shadowsocks.Keys; +import com.urbanspork.common.protocol.shadowsocks.aead.AEAD; +import com.urbanspork.common.protocol.socks.Address; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.bouncycastle.crypto.InvalidCipherTextException; + +import java.net.InetSocketAddress; +import java.util.List; + +/** + * AEAD Cipher Codec + * + * @author Zmax0 + * @see AEAD ciphers + */ +class AeadCipherCodecImpl implements AeadCipherCodec { + + private final Keys keys; + private final CipherMethod cipherMethod; + + public AeadCipherCodecImpl(CipherMethod cipherMethod, Keys keys) { + this.keys = keys; + this.cipherMethod = cipherMethod; + } + + @Override + public void encode(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException { + InetSocketAddress address = context.address(); + byte[] salt = context.control().salt(); + out.writeBytes(salt); + ByteBuf temp = Unpooled.buffer(Address.getLength(address)); + Address.encode(address, temp); + AEAD.UDP.newPayloadEncoder(cipherMethod, keys.encKey(), salt).encodePacket(Unpooled.wrappedBuffer(temp, msg), out); + msg.skipBytes(msg.readableBytes()); + } + + @Override + public void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + byte[] salt = context.control().salt(); + in.readBytes(salt); + ByteBuf packet = AEAD.UDP.newPayloadDecoder(cipherMethod, keys.encKey(), salt).decodePacket(in); + Address.decode(packet, out); + out.add(packet.slice()); + } +} \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecs.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecs.java new file mode 100644 index 0000000..0fe4d7a --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecs.java @@ -0,0 +1,22 @@ +package com.urbanspork.common.codec.shadowsocks.udp; + +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.aead.CipherMethods; +import com.urbanspork.common.codec.shadowsocks.Keys; +import com.urbanspork.common.config.ServerConfig; + +class AeadCipherCodecs { + + private AeadCipherCodecs() {} + + static AeadCipherCodec get(ServerConfig config) { + CipherKind kind = config.getCipher(); + Keys keys = Keys.from(kind, config.getPassword()); + CipherMethods methods = CipherKind.chacha20_poly1305 == kind ? CipherMethods.CHACHA20_POLY1305 : CipherMethods.AES_GCM; + if (kind.isAead2022()) { + return new Aead2022CipherCodecImpl(kind, methods.get(), keys); + } else { + return new AeadCipherCodecImpl(methods.get(), keys); + } + } +} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Context.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Context.java new file mode 100644 index 0000000..cdb30bb --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Context.java @@ -0,0 +1,9 @@ +package com.urbanspork.common.codec.shadowsocks.udp; + +import com.urbanspork.common.codec.shadowsocks.Mode; +import com.urbanspork.common.manage.shadowsocks.ServerUserManager; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Control; + +import java.net.InetSocketAddress; + +record Context(Mode mode, Control control, InetSocketAddress address, ServerUserManager userManager) {} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/UDPReplayCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/UDPReplayCodec.java similarity index 62% rename from urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/UDPReplayCodec.java rename to urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/UDPReplayCodec.java index 2bb9dd3..6a1c6d6 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/UDPReplayCodec.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/UDPReplayCodec.java @@ -1,18 +1,16 @@ -package com.urbanspork.common.codec.shadowsocks; +package com.urbanspork.common.codec.shadowsocks.udp; +import com.urbanspork.common.codec.shadowsocks.Mode; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.manage.shadowsocks.ServerUserManager; -import com.urbanspork.common.protocol.network.Network; import com.urbanspork.common.protocol.network.TernaryDatagramPacket; -import com.urbanspork.common.protocol.socks.Socks5; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Control; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.socket.DatagramPacket; import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.MessageToMessageCodec; -import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; -import io.netty.handler.codec.socksx.v5.Socks5CommandType; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -20,17 +18,15 @@ public class UDPReplayCodec extends MessageToMessageCodec { - private final AEADCipherCodec cipher; + private final AeadCipherCodec cipher; private final Mode mode; - private final Session session; private final Control control; private final ServerUserManager userManager; public UDPReplayCodec(ServerConfig config, Mode mode) { - this.cipher = AEADCipherCodecs.get(config); + this.cipher = AeadCipherCodecs.get(config); this.mode = mode; - this.session = Session.udp(config.getCipher()); - this.control = new Control(); + this.control = new Control(config.getCipher()); this.userManager = Mode.Server == mode ? ServerUserManager.DEFAULT : ServerUserManager.EMPTY; } @@ -42,20 +38,15 @@ protected void encode(ChannelHandlerContext ctx, TernaryDatagramPacket msg, List } ByteBuf in = Unpooled.buffer(); DatagramPacket data = msg.packet(); - Socks5CommandRequest request = Socks5.toCommandRequest(Socks5CommandType.CONNECT, data.recipient()); - cipher.encode(new Context(Network.UDP, mode, session, control, request, userManager), data.content(), in); - if (Mode.Client == mode) { - session.increasePacketId(1); - } else { - control.increasePacketId(1); - } + cipher.encode(new Context(mode, control, data.recipient(), userManager), data.content(), in); + control.increasePacketId(1); out.add(new DatagramPacket(in, proxy, data.sender())); } @Override protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List out) throws Exception { List list = new ArrayList<>(2); - cipher.decode(new Context(Network.UDP, mode, session, control, null, userManager), msg.content(), list); + cipher.decode(new Context(mode, control, null, userManager), msg.content(), list); out.add(new DatagramPacket((ByteBuf) list.get(1), (InetSocketAddress) list.get(0), msg.sender())); } } \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/AEAD2022.java b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/AEAD2022.java index 05b9d99..534bbfe 100644 --- a/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/AEAD2022.java +++ b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/AEAD2022.java @@ -9,10 +9,8 @@ import com.urbanspork.common.codec.aead.PayloadDecoder; import com.urbanspork.common.codec.aead.PayloadEncoder; import com.urbanspork.common.codec.chunk.AEADChunkSizeParser; -import com.urbanspork.common.codec.shadowsocks.Context; import com.urbanspork.common.codec.shadowsocks.Keys; import com.urbanspork.common.codec.shadowsocks.Mode; -import com.urbanspork.common.codec.shadowsocks.UdpCipher; import com.urbanspork.common.crypto.AES; import com.urbanspork.common.crypto.Digests; import com.urbanspork.common.manage.shadowsocks.ServerUser; @@ -141,20 +139,20 @@ static PayloadDecoder newPayloadDecoder(CipherMethod cipherMethod, byte[] key, b return new PayloadDecoder(auth, sizeCodec, EmptyPaddingLengthGenerator.INSTANCE); } - static PayloadDecoder newPayloadDecoder(CipherMethod cipherMethod, Context context, byte[] key, byte[] salt, byte[] eih) { + static PayloadDecoder newPayloadDecoder(CipherMethod cipherMethod, Session session, ServerUserManager userManager, byte[] key, byte[] salt, byte[] eih) { byte[] identitySubKey = deriveKey("shadowsocks 2022 identity subkey".getBytes(), concat(key, salt)); byte[] userHash = AES.INSTANCE.decrypt(identitySubKey, eih); if (logger.isTraceEnabled()) { logger.trace("server EIH {}, hash: {}", ByteString.valueOf(eih), ByteString.valueOf(userHash)); } - ServerUser user = context.userManager().getUserByHash(userHash); + ServerUser user = userManager.getUserByHash(userHash); if (user == null) { throw new DecoderException("invalid client user identity " + ByteString.valueOf(userHash)); } else { if (logger.isTraceEnabled()) { logger.trace("{} chosen by EIH", user); } - context.session().setUser(user); + session.setUser(user); return newPayloadDecoder(cipherMethod, user.key(), salt); } } @@ -233,7 +231,7 @@ static int getNonceLength(CipherKind kind) { } static UdpCipher getCipher(CipherKind kind, CipherMethod method, byte[] key, long sessionId) { - return UDPCipherCaches.INSTANCE.get(kind, method, key, sessionId); + return UdpCipherCaches.INSTANCE.get(kind, method, key, sessionId); } static void encodePacket(UdpCipher cipher, byte[] iPSK, int eihLength, ByteBuf in, ByteBuf out) throws InvalidCipherTextException { @@ -250,7 +248,7 @@ static void encodePacket(UdpCipher cipher, byte[] iPSK, int eihLength, ByteBuf i out.writeBytes(cipher.seal(encrypting, nonce)); } - static ByteBuf decodePacket(CipherKind kind, CipherMethod method, Context context, byte[] key, ByteBuf in) throws InvalidCipherTextException { + static ByteBuf decodePacket(CipherKind kind, CipherMethod method, Control control, ServerUserManager userManager, byte[] key, ByteBuf in) throws InvalidCipherTextException { byte[] header = new byte[16]; in.readBytes(header); AES.INSTANCE.decrypt(key, header, header); @@ -258,7 +256,6 @@ static ByteBuf decodePacket(CipherKind kind, CipherMethod method, Context contex long sessionId = headerBuffer.getLong(0); UdpCipher cipher; byte[] eih; - ServerUserManager userManager = context.userManager(); if (kind.supportEih() && userManager.userCount() > 0) { eih = new byte[16]; in.readBytes(eih); @@ -275,7 +272,7 @@ static ByteBuf decodePacket(CipherKind kind, CipherMethod method, Context contex } else { logger.trace("{} chosen by EIH", user); cipher = getCipher(kind, method, user.key(), sessionId); - context.session().setUser(user); + control.setUser(user); } } else { eih = new byte[0]; diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Control.java b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/Control.java similarity index 75% rename from urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Control.java rename to urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/Control.java index 8210b6f..f466290 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/Control.java +++ b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/Control.java @@ -1,20 +1,24 @@ -package com.urbanspork.common.codec.shadowsocks; +package com.urbanspork.common.protocol.shadowsocks.aead2022; +import com.urbanspork.common.codec.CipherKind; import com.urbanspork.common.manage.shadowsocks.ServerUser; +import com.urbanspork.common.util.Dice; import java.util.concurrent.ThreadLocalRandom; public class Control { + private final byte[] salt; private long clientSessionId; private long serverSessionId; private long packetId; private ServerUser user; - public Control() { - this(0, 0, 0, null); + public Control(CipherKind kind) { + this(Dice.rollBytes(kind.keySize()), ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong(), 0, null); } - public Control(long clientSessionId, long serverSessionId, long packetId, ServerUser user) { + Control(byte[] salt, long clientSessionId, long serverSessionId, long packetId, ServerUser user) { + this.salt = salt; this.clientSessionId = clientSessionId; this.serverSessionId = serverSessionId; this.packetId = packetId; @@ -34,6 +38,10 @@ public void increasePacketId(long i) { } } + public byte[] salt() { + return salt; + } + public long getPacketId() { return packetId; } diff --git a/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/Session.java b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/Session.java new file mode 100644 index 0000000..9ac0f02 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/Session.java @@ -0,0 +1,46 @@ +package com.urbanspork.common.protocol.shadowsocks.aead2022; + +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.manage.shadowsocks.ServerUser; +import com.urbanspork.common.util.ByteString; +import com.urbanspork.common.util.Dice; + +public class Session { + private final byte[] salt; + private byte[] requestSalt; + private ServerUser user; + + Session(byte[] salt, byte[] requestSalt) { + this.salt = salt; + this.requestSalt = requestSalt; + } + + public Session(CipherKind kind) { + this(Dice.rollBytes(kind.keySize()), null); + } + + public byte[] salt() { + return salt; + } + + public byte[] getRequestSalt() { + return requestSalt; + } + + public void setRequestSalt(byte[] requestSalt) { + this.requestSalt = requestSalt; + } + + public ServerUser getUser() { + return user; + } + + public void setUser(ServerUser user) { + this.user = user; + } + + @Override + public String toString() { + return String.format("S:%s, RS:%s", ByteString.valueOf(salt), ByteString.valueOf(requestSalt)); + } +} diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/UdpCipher.java b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipher.java similarity index 88% rename from urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/UdpCipher.java rename to urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipher.java index 99d81b7..e459f63 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/UdpCipher.java +++ b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipher.java @@ -1,4 +1,4 @@ -package com.urbanspork.common.codec.shadowsocks; +package com.urbanspork.common.protocol.shadowsocks.aead2022; import com.urbanspork.common.codec.aead.CipherMethod; import org.bouncycastle.crypto.InvalidCipherTextException; diff --git a/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCache.java b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipherCache.java similarity index 93% rename from urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCache.java rename to urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipherCache.java index 8780183..b1aa98c 100644 --- a/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCache.java +++ b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipherCache.java @@ -2,7 +2,6 @@ import com.urbanspork.common.codec.CipherKind; import com.urbanspork.common.codec.aead.CipherMethod; -import com.urbanspork.common.codec.shadowsocks.UdpCipher; import io.netty.util.HashedWheelTimer; import java.time.Duration; @@ -12,7 +11,7 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; -public class UDPCipherCache { +public class UdpCipherCache { private final HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.SECONDS); private final LinkedHashMap map = new LinkedHashMap<>() { @Override @@ -23,7 +22,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { private final Duration duration; private final int limit; - public UDPCipherCache(Duration duration, int limit) { + public UdpCipherCache(Duration duration, int limit) { this.duration = duration; this.limit = limit; } diff --git a/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCaches.java b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipherCaches.java similarity index 63% rename from urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCaches.java rename to urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipherCaches.java index cdd0b14..30bc8d2 100644 --- a/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCaches.java +++ b/urban-spork-common/src/com/urbanspork/common/protocol/shadowsocks/aead2022/UdpCipherCaches.java @@ -2,14 +2,13 @@ import com.urbanspork.common.codec.CipherKind; import com.urbanspork.common.codec.aead.CipherMethod; -import com.urbanspork.common.codec.shadowsocks.UdpCipher; -public enum UDPCipherCaches { - INSTANCE(new UDPCipherCache(AEAD2022.UDP.CIPHER_CACHE_DURATION, AEAD2022.UDP.CIPHER_CACHE_LIMIT)); +public enum UdpCipherCaches { + INSTANCE(new UdpCipherCache(AEAD2022.UDP.CIPHER_CACHE_DURATION, AEAD2022.UDP.CIPHER_CACHE_LIMIT)); - private final UDPCipherCache cache; + private final UdpCipherCache cache; - UDPCipherCaches(UDPCipherCache cache) { + UdpCipherCaches(UdpCipherCache cache) { this.cache = cache; } diff --git a/urban-spork-server/src/com/urbanspork/server/Server.java b/urban-spork-server/src/com/urbanspork/server/Server.java index 65b60c5..3911c24 100644 --- a/urban-spork-server/src/com/urbanspork/server/Server.java +++ b/urban-spork-server/src/com/urbanspork/server/Server.java @@ -2,9 +2,12 @@ import com.urbanspork.common.channel.ExceptionHandler; import com.urbanspork.common.codec.shadowsocks.Mode; -import com.urbanspork.common.codec.shadowsocks.UDPReplayCodec; +import com.urbanspork.common.codec.shadowsocks.udp.UDPReplayCodec; import com.urbanspork.common.config.ConfigHandler; import com.urbanspork.common.config.ServerConfig; +import com.urbanspork.common.config.ServerUserConfig; +import com.urbanspork.common.manage.shadowsocks.ServerUser; +import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.Protocols; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; @@ -68,20 +71,26 @@ public static void launch(List configs, Promise promise) throws InterruptedException { int port = config.getPort(); - if (Protocols.shadowsocks == config.getProtocol() && config.udpEnabled()) { - new Bootstrap().group(bossGroup).channel(NioDatagramChannel.class) - .option(ChannelOption.SO_BROADCAST, true) - .handler(new ChannelInitializer<>() { - @Override - protected void initChannel(Channel ch) { - ch.pipeline().addLast( - new UDPReplayCodec(config, Mode.Server), - new ServerUDPReplayHandler(config.getPacketEncoding(), workerGroup), - new ExceptionHandler(config) - ); - } - }) - .bind(port).sync().addListener(future -> logger.info("Startup udp server => {}", config)); + if (Protocols.shadowsocks == config.getProtocol()) { + List user = config.getUser(); + if (user != null) { + user.stream().map(ServerUser::from).forEach(ServerUserManager.DEFAULT::addUser); + } + if (config.udpEnabled()) { + new Bootstrap().group(bossGroup).channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast( + new UDPReplayCodec(config, Mode.Server), + new ServerUDPReplayHandler(config.getPacketEncoding(), workerGroup), + new ExceptionHandler(config) + ); + } + }) + .bind(port).sync().addListener(future -> logger.info("Startup udp server => {}", config)); + } } new ServerBootstrap().group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) diff --git a/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java b/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java index 8ce5add..3f2beed 100644 --- a/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java +++ b/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java @@ -2,7 +2,7 @@ import com.urbanspork.common.channel.ExceptionHandler; import com.urbanspork.common.codec.shadowsocks.Mode; -import com.urbanspork.common.codec.shadowsocks.TCPReplayCodec; +import com.urbanspork.common.codec.shadowsocks.tcp.TCPReplayCodec; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.protocol.Protocols; import com.urbanspork.server.vmess.ServerAEADCodec; diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecTestCase.java deleted file mode 100644 index ff19ddc..0000000 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecTestCase.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.urbanspork.common.codec.shadowsocks; - -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import com.urbanspork.common.codec.CipherKind; -import com.urbanspork.common.codec.aead.CipherMethod; -import com.urbanspork.common.codec.aead.CipherMethods; -import com.urbanspork.common.codec.aead.PayloadDecoder; -import com.urbanspork.common.codec.aead.PayloadEncoder; -import com.urbanspork.common.config.ServerConfig; -import com.urbanspork.common.manage.shadowsocks.ServerUserManager; -import com.urbanspork.common.protocol.Protocols; -import com.urbanspork.common.protocol.network.Network; -import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; -import com.urbanspork.common.util.Dice; -import com.urbanspork.test.TestDice; -import com.urbanspork.test.template.TraceLevelLoggerTestTemplate; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandRequest; -import io.netty.handler.codec.socksx.v5.Socks5AddressType; -import io.netty.handler.codec.socksx.v5.Socks5CommandType; -import org.bouncycastle.crypto.InvalidCipherTextException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; - -@DisplayName("Shadowsocks - AEAD Cipher Codec") -class AEADCipherCodecTestCase extends TraceLevelLoggerTestTemplate { - - @Test - void testIncorrectPassword() { - String password = Base64.getEncoder().encodeToString(Dice.rollBytes(10)); - ServerConfig config = new ServerConfig(); - config.setPassword(password); - config.setCipher(CipherKind.aead2022_blake3_aes_128_gcm); - Assertions.assertThrows(IllegalArgumentException.class, () -> AEADCipherCodecs.get(config)); - } - - @Test - void testTooShortHeader() { - AEADCipherCodec codec = newAEADCipherCodec(); - List out = new ArrayList<>(); - ByteBuf in = Unpooled.wrappedBuffer(Dice.rollBytes(3)); - Context context = new Context(Network.UDP, Mode.Client, TestDice.rollCipher(), null, ServerUserManager.EMPTY); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(context, in, out)); - } - - @Test - void testEmptyMsg() throws InvalidCipherTextException { - AEADCipherCodec codec = newAEADCipherCodec(); - DefaultSocks5CommandRequest request = new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, Socks5AddressType.DOMAIN, TestDice.rollHost(), TestDice.rollPort()); - List out = new ArrayList<>(); - ByteBuf in = Unpooled.buffer(); - CipherKind kind = TestDice.rollCipher(); - codec.encode(new Context(Network.UDP, Mode.Client, kind, request, ServerUserManager.EMPTY), Unpooled.EMPTY_BUFFER, in); - Assertions.assertTrue(in.isReadable()); - codec.decode(new Context(Network.UDP, Mode.Server, kind, request, ServerUserManager.EMPTY), in, out); - Assertions.assertFalse(in.isReadable()); - Assertions.assertFalse(out.isEmpty()); - } - - @Test - void testUnexpectedStreamType() throws InvalidCipherTextException { - DefaultSocks5CommandRequest request = new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, Socks5AddressType.DOMAIN, TestDice.rollHost(), TestDice.rollPort()); - CipherKind kind = CipherKind.aead2022_blake3_aes_128_gcm; - int saltSize = 16; - String password = TestDice.rollPassword(Protocols.shadowsocks, kind); - CipherMethod method = CipherMethods.AES_GCM.get(); - ServerConfig config = new ServerConfig(); - config.setPassword(password); - config.setCipher(kind); - AEADCipherCodec codec = AEADCipherCodecs.get(config); - ByteBuf msg = Unpooled.buffer(); - codec.encode(new Context(Network.TCP, Mode.Client, kind, request, ServerUserManager.EMPTY), Unpooled.wrappedBuffer(Dice.rollBytes(10)), msg); - byte[] salt = new byte[saltSize]; - msg.readBytes(salt); - byte[] passwordBytes = Base64.getDecoder().decode(password); - PayloadDecoder decoder = AEAD2022.TCP.newPayloadDecoder(method, passwordBytes, salt); - byte[] decryptedHeader = new byte[1 + 8 + 2 + method.tagSize()]; - msg.readBytes(decryptedHeader); - byte[] header = decoder.auth().open(decryptedHeader); - header[0] = 1; - PayloadEncoder encoder = AEAD2022.TCP.newPayloadEncoder(method, passwordBytes, salt); - ByteBuf temp = Unpooled.buffer(); - temp.writeBytes(salt); - temp.writeBytes(encoder.auth().seal(header)); - temp.writeBytes(msg); - ArrayList out = new ArrayList<>(); - Context context = new Context(Network.TCP, Mode.Server, kind, request, ServerUserManager.EMPTY); - codec.decode(context, msg, out); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(context, temp, out)); - } - - @Test - void testTooShortPacket() { - AEADCipherCodec codec = newAEADCipherCodec(); - ByteBuf in = Unpooled.buffer(); - List out = new ArrayList<>(); - Context c1 = new Context(Network.UDP, Mode.Client, CipherKind.aead2022_blake3_aes_128_gcm, null, ServerUserManager.EMPTY); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(c1, in, out)); - Context c2 = new Context(Network.UDP, Mode.Server, CipherKind.aead2022_blake3_aes_128_gcm, null, ServerUserManager.EMPTY); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(c2, in, out)); - } - - @Test - void testInvalidSocketType() throws InvalidCipherTextException { - DefaultSocks5CommandRequest request = new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, Socks5AddressType.DOMAIN, TestDice.rollHost(), TestDice.rollPort()); - Context c1 = new Context(Network.UDP, Mode.Client, CipherKind.aead2022_blake3_aes_128_gcm, request, ServerUserManager.EMPTY); - testInvalidSocketType(c1); - Context c2 = new Context(Network.UDP, Mode.Server, CipherKind.aead2022_blake3_aes_128_gcm, request, ServerUserManager.EMPTY); - testInvalidSocketType(c2); - } - - private static void testInvalidSocketType(Context c) throws InvalidCipherTextException { - AEADCipherCodec codec = newAEADCipherCodec(); - byte[] msg = Dice.rollBytes(10); - ByteBuf in = Unpooled.buffer(); - in.writeBytes(msg); - ByteBuf out = Unpooled.buffer(); - codec.encode(c, in, out); - ArrayList list = new ArrayList<>(); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(c, out, list)); - } - - static AEADCipherCodec newAEADCipherCodec() { - CipherKind kind = CipherKind.aead2022_blake3_aes_128_gcm; - ServerConfig config = new ServerConfig(); - config.setPassword(TestDice.rollPassword(Protocols.shadowsocks, kind)); - config.setCipher(kind); - return AEADCipherCodecs.get(config); - } - - @Override - protected Logger logger() { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - return loggerContext.getLogger(AEADCipherCodec.class); - } -} \ No newline at end of file diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java index 7e92d57..1b98717 100644 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java @@ -1,6 +1,8 @@ package com.urbanspork.common.codec.shadowsocks; import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.shadowsocks.tcp.TCPReplayCodec; +import com.urbanspork.common.codec.shadowsocks.udp.UDPReplayCodec; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.protocol.Protocols; import com.urbanspork.common.protocol.network.TernaryDatagramPacket; diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/SessionTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/SessionTestCase.java deleted file mode 100644 index fee90cc..0000000 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/SessionTestCase.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.urbanspork.common.codec.shadowsocks; - -import com.urbanspork.common.protocol.network.Network; -import com.urbanspork.common.util.Dice; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.BiConsumer; -import java.util.function.Function; - -@DisplayName("Shadowsocks - Session") -class SessionTestCase { - @Test - void testGetterAndSetter() { - byte[] salt = Dice.rollBytes(32); - byte[] requestSalt = Dice.rollBytes(32); - Network network = ThreadLocalRandom.current().nextBoolean() ? Network.UDP : Network.TCP; - Session session = new Session(network, 0, 0, 0, salt, null); - long id = ThreadLocalRandom.current().nextLong(); - testGetterAndSetter(id, session, Session::getPacketId, Session::setPacketId); - testGetterAndSetter(id, session, Session::getClientSessionId, Session::setClientSessionId); - testGetterAndSetter(id, session, Session::getServerSessionId, Session::setServerSessionId); - testGetterAndSetter(requestSalt, session, Session::getRequestSalt, Session::setRequestSalt); - Assertions.assertArrayEquals(salt, session.salt()); - Assertions.assertArrayEquals(requestSalt, session.getRequestSalt()); - } - - @Test - void testIncreasePacketId() { - Control control = new Control(1, 1, Long.MAX_VALUE, null); - control.increasePacketId(1); - Assertions.assertEquals(0, control.getPacketId()); - Assertions.assertNotEquals(1, control.getClientSessionId()); - } - - private static void testGetterAndSetter(U u, T t, Function getter, BiConsumer setter) { - Assertions.assertNotEquals(u, getter.apply(t)); - setter.accept(t, u); - Assertions.assertEquals(u, getter.apply(t)); - } -} diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AEADCipherCodecTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AEADCipherCodecTestCase.java new file mode 100644 index 0000000..f425f7e --- /dev/null +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AEADCipherCodecTestCase.java @@ -0,0 +1,83 @@ +package com.urbanspork.common.codec.shadowsocks.tcp; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.aead.CipherMethod; +import com.urbanspork.common.codec.aead.CipherMethods; +import com.urbanspork.common.codec.aead.PayloadDecoder; +import com.urbanspork.common.codec.aead.PayloadEncoder; +import com.urbanspork.common.codec.shadowsocks.Mode; +import com.urbanspork.common.config.ServerConfig; +import com.urbanspork.common.manage.shadowsocks.ServerUserManager; +import com.urbanspork.common.protocol.Protocols; +import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Session; +import com.urbanspork.common.util.Dice; +import com.urbanspork.test.TestDice; +import com.urbanspork.test.template.TraceLevelLoggerTestTemplate; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandRequest; +import io.netty.handler.codec.socksx.v5.Socks5AddressType; +import io.netty.handler.codec.socksx.v5.Socks5CommandType; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Base64; + +@DisplayName("Shadowsocks - AEAD Cipher TCP Codec") +class AEADCipherCodecTestCase extends TraceLevelLoggerTestTemplate { + + @Test + void testIncorrectPassword() { + String password = Base64.getEncoder().encodeToString(Dice.rollBytes(10)); + ServerConfig config = new ServerConfig(); + config.setPassword(password); + config.setCipher(CipherKind.aead2022_blake3_aes_128_gcm); + Assertions.assertThrows(IllegalArgumentException.class, () -> AeadCipherCodecs.get(config)); + } + + @Test + void testUnexpectedStreamType() throws InvalidCipherTextException { + DefaultSocks5CommandRequest request = new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, Socks5AddressType.DOMAIN, TestDice.rollHost(), TestDice.rollPort()); + CipherKind kind = CipherKind.aead2022_blake3_aes_128_gcm; + int saltSize = 16; + String password = TestDice.rollPassword(Protocols.shadowsocks, kind); + CipherMethod method = CipherMethods.AES_GCM.get(); + ServerConfig config = new ServerConfig(); + config.setPassword(password); + config.setCipher(kind); + AeadCipherCodec codec = AeadCipherCodecs.get(config); + ByteBuf msg = Unpooled.buffer(); + codec.encode(new Context(Mode.Client, new Session(kind), request, ServerUserManager.EMPTY), Unpooled.wrappedBuffer(Dice.rollBytes(10)), msg); + byte[] salt = new byte[saltSize]; + msg.readBytes(salt); + byte[] passwordBytes = Base64.getDecoder().decode(password); + PayloadDecoder decoder = AEAD2022.TCP.newPayloadDecoder(method, passwordBytes, salt); + byte[] decryptedHeader = new byte[1 + 8 + 2 + method.tagSize()]; + msg.readBytes(decryptedHeader); + byte[] header = decoder.auth().open(decryptedHeader); + header[0] = 1; + PayloadEncoder encoder = AEAD2022.TCP.newPayloadEncoder(method, passwordBytes, salt); + ByteBuf temp = Unpooled.buffer(); + temp.writeBytes(salt); + temp.writeBytes(encoder.auth().seal(header)); + temp.writeBytes(msg); + ArrayList out = new ArrayList<>(); + Context context = new Context(Mode.Server, new Session(kind), request, ServerUserManager.EMPTY); + codec.decode(context, msg, out); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(context, temp, out)); + } + + @Override + protected Logger logger() { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + return loggerContext.getLogger(AeadCipherCodec.class); + } +} \ No newline at end of file diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecsTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AEADCipherCodecsTestCase.java similarity index 75% rename from urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecsTestCase.java rename to urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AEADCipherCodecsTestCase.java index eef0019..90f81de 100644 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/AEADCipherCodecsTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AEADCipherCodecsTestCase.java @@ -1,19 +1,21 @@ -package com.urbanspork.common.codec.shadowsocks; +package com.urbanspork.common.codec.shadowsocks.tcp; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.shadowsocks.Mode; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.Protocols; -import com.urbanspork.common.protocol.network.Network; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Session; import com.urbanspork.common.util.Dice; import com.urbanspork.test.TestDice; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandRequest; import io.netty.handler.codec.socksx.v5.Socks5AddressType; +import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; import io.netty.handler.codec.socksx.v5.Socks5CommandType; import org.junit.jupiter.api.*; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -25,7 +27,7 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; -@DisplayName("Shadowsocks - AEAD Cipher Codecs") +@DisplayName("Shadowsocks - AEAD Cipher TCP Codecs") @TestInstance(Lifecycle.PER_CLASS) class AEADCipherCodecsTestCase { @@ -36,7 +38,7 @@ class AEADCipherCodecsTestCase { @BeforeAll void beforeAll() { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - Logger logger = loggerContext.getLogger(AEADCipherCodec.class); + Logger logger = loggerContext.getLogger(AeadCipherCodec.class); logger.setLevel(Level.TRACE); } @@ -56,13 +58,11 @@ void parameterizedTest(CipherKind kind) throws Exception { int port = TestDice.rollPort(); String host = TestDice.rollHost(); DefaultSocks5CommandRequest request = new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, Socks5AddressType.DOMAIN, host, port); - cipherTest(new Context(Network.TCP, Mode.Client, kind, request, ServerUserManager.EMPTY), new Context(Network.TCP, Mode.Server, kind, null, ServerUserManager.EMPTY), true); - cipherTest(new Context(Network.UDP, Mode.Client, kind, request, ServerUserManager.EMPTY), new Context(Network.UDP, Mode.Server, kind, null, ServerUserManager.EMPTY), false); + cipherTest(newContext(Mode.Client, kind, request), newContext(Mode.Server, kind, null)); } - - private void cipherTest(Context request, Context response, boolean reRoll) throws Exception { - List list = cipherTest(request, response, Unpooled.copiedBuffer(in), reRoll); + private void cipherTest(Context request, Context response) throws Exception { + List list = cipherTest(request, response, Unpooled.copiedBuffer(in)); byte[] out = new byte[in.length]; int len = 0; for (Object obj : list) { @@ -77,12 +77,12 @@ private void cipherTest(Context request, Context response, boolean reRoll) throw Assertions.assertArrayEquals(in, out); } - private List cipherTest(Context request, Context response, ByteBuf in, boolean reRoll) throws Exception { + private List cipherTest(Context request, Context response, ByteBuf in) throws Exception { ServerConfig config = new ServerConfig(); config.setCipher(kind); config.setPassword(password); - AEADCipherCodec client = AEADCipherCodecs.get(config); - AEADCipherCodec server = AEADCipherCodecs.get(config); + AeadCipherCodec client = AeadCipherCodecs.get(config); + AeadCipherCodec server = AeadCipherCodecs.get(config); List encodeSlices = new ArrayList<>(); for (ByteBuf slice : randomSlice(in, false)) { ByteBuf buf = Unpooled.buffer(); @@ -90,16 +90,10 @@ private List cipherTest(Context request, Context response, ByteBuf in, b encodeSlices.add(buf); } List out = new ArrayList<>(); - if (reRoll) { - ByteBuf buffer = Unpooled.buffer(); - for (ByteBuf slice : randomSlice(merge(encodeSlices), true)) { - buffer.writeBytes(slice); - server.decode(response, buffer, out); - } - } else { - for (ByteBuf buf : encodeSlices) { - server.decode(response, buf, out); - } + ByteBuf buffer = Unpooled.buffer(); + for (ByteBuf slice : randomSlice(merge(encodeSlices), true)) { + buffer.writeBytes(slice); + server.decode(response, buffer, out); } return out; } @@ -124,4 +118,9 @@ private ByteBuf merge(List list) { } return merged; } + + Context newContext(Mode mode, CipherKind kind, Socks5CommandRequest request) { + return new Context(mode, new Session(kind), request, ServerUserManager.EMPTY); + } + } \ No newline at end of file diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecTestCase.java new file mode 100644 index 0000000..9c00cba --- /dev/null +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecTestCase.java @@ -0,0 +1,107 @@ +package com.urbanspork.common.codec.shadowsocks.udp; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.shadowsocks.Mode; +import com.urbanspork.common.config.ServerConfig; +import com.urbanspork.common.manage.shadowsocks.ServerUserManager; +import com.urbanspork.common.protocol.Protocols; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Control; +import com.urbanspork.common.util.Dice; +import com.urbanspork.test.TestDice; +import com.urbanspork.test.template.TraceLevelLoggerTestTemplate; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DecoderException; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +@DisplayName("Shadowsocks - AEAD Cipher UDP Codec") +class AEADCipherCodecTestCase extends TraceLevelLoggerTestTemplate { + + @Test + void testIncorrectPassword() { + String password = Base64.getEncoder().encodeToString(Dice.rollBytes(10)); + ServerConfig config = new ServerConfig(); + config.setPassword(password); + config.setCipher(CipherKind.aead2022_blake3_aes_128_gcm); + Assertions.assertThrows(IllegalArgumentException.class, () -> AeadCipherCodecs.get(config)); + } + + @Test + void testTooShortHeader() { + AeadCipherCodec codec = newAEADCipherCodec(); + List out = new ArrayList<>(); + ByteBuf in = Unpooled.wrappedBuffer(Dice.rollBytes(3)); + Context context = new Context(Mode.Client, new Control(TestDice.rollCipher()), null, ServerUserManager.EMPTY); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(context, in, out)); + } + + @Test + void testEmptyMsg() throws InvalidCipherTextException { + AeadCipherCodec codec = newAEADCipherCodec(); + InetSocketAddress address = InetSocketAddress.createUnresolved(TestDice.rollHost(), TestDice.rollPort()); + List out = new ArrayList<>(); + ByteBuf in = Unpooled.buffer(); + CipherKind kind = TestDice.rollCipher(); + codec.encode(new Context(Mode.Client, new Control(kind), address, ServerUserManager.EMPTY), Unpooled.EMPTY_BUFFER, in); + Assertions.assertTrue(in.isReadable()); + codec.decode(new Context(Mode.Server, new Control(kind), address, ServerUserManager.EMPTY), in, out); + Assertions.assertFalse(in.isReadable()); + Assertions.assertFalse(out.isEmpty()); + } + + @Test + void testTooShortPacket() { + AeadCipherCodec codec = newAEADCipherCodec(); + ByteBuf in = Unpooled.buffer(); + List out = new ArrayList<>(); + Context c1 = new Context(Mode.Client, new Control(CipherKind.aead2022_blake3_aes_128_gcm), null, ServerUserManager.EMPTY); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(c1, in, out)); + Context c2 = new Context(Mode.Server, new Control(CipherKind.aead2022_blake3_aes_128_gcm), null, ServerUserManager.EMPTY); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(c2, in, out)); + } + + @Test + void testInvalidSocketType() throws InvalidCipherTextException { + InetSocketAddress address = InetSocketAddress.createUnresolved(TestDice.rollHost(), TestDice.rollPort()); + Context c1 = new Context(Mode.Client, new Control(CipherKind.aead2022_blake3_aes_128_gcm), address, ServerUserManager.EMPTY); + testInvalidSocketType(c1); + Context c2 = new Context(Mode.Server, new Control(CipherKind.aead2022_blake3_aes_128_gcm), address, ServerUserManager.EMPTY); + testInvalidSocketType(c2); + } + + private static void testInvalidSocketType(Context c) throws InvalidCipherTextException { + AeadCipherCodec codec = newAEADCipherCodec(); + byte[] msg = Dice.rollBytes(10); + ByteBuf in = Unpooled.buffer(); + in.writeBytes(msg); + ByteBuf out = Unpooled.buffer(); + codec.encode(c, in, out); + ArrayList list = new ArrayList<>(); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(c, out, list)); + } + + static AeadCipherCodec newAEADCipherCodec() { + CipherKind kind = CipherKind.aead2022_blake3_aes_128_gcm; + ServerConfig config = new ServerConfig(); + config.setPassword(TestDice.rollPassword(Protocols.shadowsocks, kind)); + config.setCipher(kind); + return AeadCipherCodecs.get(config); + } + + @Override + protected Logger logger() { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + return loggerContext.getLogger(AeadCipherCodec.class); + } +} \ No newline at end of file diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecsTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecsTestCase.java new file mode 100644 index 0000000..c9cd339 --- /dev/null +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AEADCipherCodecsTestCase.java @@ -0,0 +1,107 @@ +package com.urbanspork.common.codec.shadowsocks.udp; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.codec.shadowsocks.Mode; +import com.urbanspork.common.config.ServerConfig; +import com.urbanspork.common.manage.shadowsocks.ServerUserManager; +import com.urbanspork.common.protocol.Protocols; +import com.urbanspork.common.protocol.shadowsocks.aead2022.Control; +import com.urbanspork.common.util.Dice; +import com.urbanspork.test.TestDice; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +@DisplayName("Shadowsocks - AEAD Cipher UDP Codecs") +@TestInstance(Lifecycle.PER_CLASS) +class AEADCipherCodecsTestCase { + + private final byte[] in = Dice.rollBytes(0xffff * 10); + private CipherKind kind; + private String password; + + @BeforeAll + void beforeAll() { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + Logger logger = loggerContext.getLogger(AeadCipherCodec.class); + logger.setLevel(Level.TRACE); + } + + @DisplayName("Single cipher") + @Test + void test() throws Exception { + parameterizedTest(CipherKind.chacha20_poly1305); + parameterizedTest(CipherKind.aead2022_blake3_aes_256_gcm); + } + + @ParameterizedTest + @DisplayName("All supported cipher iterate") + @EnumSource(CipherKind.class) + void parameterizedTest(CipherKind kind) throws Exception { + this.password = TestDice.rollPassword(Protocols.shadowsocks, kind); + this.kind = kind; + int port = TestDice.rollPort(); + String host = TestDice.rollHost(); + InetSocketAddress address = InetSocketAddress.createUnresolved(host, port); + cipherTest(new Context(Mode.Client, new Control(kind), address, ServerUserManager.EMPTY), new Context(Mode.Server, new Control(kind), null, ServerUserManager.EMPTY)); + } + + private void cipherTest(Context request, Context response) throws Exception { + List list = cipherTest(request, response, Unpooled.copiedBuffer(in)); + byte[] out = new byte[in.length]; + int len = 0; + for (Object obj : list) { + if (obj instanceof ByteBuf outBuf) { + int readableBytes = outBuf.readableBytes(); + outBuf.readBytes(out, len, readableBytes); + outBuf.release(); + len += readableBytes; + } + } + Assertions.assertEquals(in.length, len); + Assertions.assertArrayEquals(in, out); + } + + private List cipherTest(Context request, Context response, ByteBuf in) + throws Exception { + ServerConfig config = new ServerConfig(); + config.setCipher(kind); + config.setPassword(password); + AeadCipherCodec client = AeadCipherCodecs.get(config); + AeadCipherCodec server = AeadCipherCodecs.get(config); + List encodeSlices = new ArrayList<>(); + for (ByteBuf slice : randomSlice(in)) { + ByteBuf buf = Unpooled.buffer(); + client.encode(request, slice, buf); + encodeSlices.add(buf); + } + List out = new ArrayList<>(); + for (ByteBuf buf : encodeSlices) { + server.decode(response, buf, out); + } + return out; + } + + private List randomSlice(ByteBuf src) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List list = new ArrayList<>(); + while (src.isReadable()) { + int maxChunkSize = kind.isAead2022() ? 0xffff - 100 : 0x3fff - 100; + list.add(src.readSlice(Math.min(src.readableBytes(), random.nextInt(1, maxChunkSize)))); + } + return list; + } + +} \ No newline at end of file diff --git a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/AEAD2022TestCase.java b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/Aead2022TestCase.java similarity index 90% rename from urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/AEAD2022TestCase.java rename to urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/Aead2022TestCase.java index 4e5a829..3bf2736 100644 --- a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/AEAD2022TestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/Aead2022TestCase.java @@ -5,14 +5,11 @@ import com.urbanspork.common.codec.CipherKind; import com.urbanspork.common.codec.aead.CipherMethod; import com.urbanspork.common.codec.aead.CipherMethods; -import com.urbanspork.common.codec.shadowsocks.Context; import com.urbanspork.common.codec.shadowsocks.Keys; -import com.urbanspork.common.codec.shadowsocks.Mode; import com.urbanspork.common.config.ServerUserConfig; import com.urbanspork.common.manage.shadowsocks.ServerUser; import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.Protocols; -import com.urbanspork.common.protocol.network.Network; import com.urbanspork.common.util.Dice; import com.urbanspork.test.TestDice; import com.urbanspork.test.template.TraceLevelLoggerTestTemplate; @@ -31,7 +28,7 @@ import java.util.concurrent.ThreadLocalRandom; @DisplayName("Shadowsocks - AEAD 2022") -class AEAD2022TestCase extends TraceLevelLoggerTestTemplate { +class Aead2022TestCase extends TraceLevelLoggerTestTemplate { @Test void testSessionSubkey() { byte[] key = Base64.getDecoder().decode("Lc3tTx0BY6ZJ/fCwOx3JvF0I/anhwJBO5p2+FA5Vce4="); @@ -78,11 +75,10 @@ void testTcpInvalidClientUserIdentity() { ServerUser user = rollUser(kind); ServerUserManager userManager = ServerUserManager.DEFAULT; userManager.addUser(user); - Context context = new Context(Network.UDP, Mode.Server, null, null, null, userManager); byte[] key = Dice.rollBytes(kind.keySize()); byte[] salt = Dice.rollBytes(kind.keySize()); byte[] eih = Dice.rollBytes(16); - Assertions.assertThrows(DecoderException.class, () -> AEAD2022.TCP.newPayloadDecoder(method, context, key, salt, eih)); + Assertions.assertThrows(DecoderException.class, () -> AEAD2022.TCP.newPayloadDecoder(method, null, userManager, key, salt, eih)); userManager.removeUserByHash(user.identityHash()); } @@ -104,8 +100,8 @@ void testUdpUserNotFound() throws InvalidCipherTextException { in.writeCharSequence(msg, StandardCharsets.US_ASCII); ByteBuf out = Unpooled.buffer(); AEAD2022.UDP.encodePacket(AEAD2022.UDP.getCipher(kind, method, iPSK, sessionId), iPSK, 16, in, out); - Context context = new Context(Network.UDP, Mode.Server, null, null, null, userManager); - Assertions.assertThrows(DecoderException.class, () -> AEAD2022.UDP.decodePacket(kind, method, context, iPSK, out)); + Control control = new Control(kind); + Assertions.assertThrows(DecoderException.class, () -> AEAD2022.UDP.decodePacket(kind, method, control, userManager, iPSK, out)); userManager.removeUserByHash(user.identityHash()); } diff --git a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/ControlTestCase.java b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/ControlTestCase.java new file mode 100644 index 0000000..2b2e752 --- /dev/null +++ b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/ControlTestCase.java @@ -0,0 +1,31 @@ +package com.urbanspork.common.protocol.shadowsocks.aead2022; + +import com.urbanspork.common.util.Dice; +import com.urbanspork.test.TestUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadLocalRandom; + +@DisplayName("Shadowsocks - Control") +class ControlTestCase { + @Test + void testIncreasePacketId() { + Control control = new Control(null, 1, 1, Long.MAX_VALUE, null); + control.increasePacketId(1); + Assertions.assertEquals(0, control.getPacketId()); + Assertions.assertNotEquals(1, control.getClientSessionId()); + } + + @Test + void testGetterAndSetter() { + byte[] salt = Dice.rollBytes(32); + long id = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); + Control control = new Control(salt, 0, 0, 0, null); + TestUtil.testGetterAndSetter(id, control, Control::getClientSessionId, Control::setClientSessionId); + TestUtil.testGetterAndSetter(id, control, Control::getServerSessionId, Control::setServerSessionId); + TestUtil.testGetterAndSetter(id, control, Control::getPacketId, Control::setPacketId); + Assertions.assertArrayEquals(salt, control.salt()); + } +} diff --git a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/SessionTestCase.java b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/SessionTestCase.java new file mode 100644 index 0000000..dd9f33b --- /dev/null +++ b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/SessionTestCase.java @@ -0,0 +1,20 @@ +package com.urbanspork.common.protocol.shadowsocks.aead2022; + +import com.urbanspork.common.util.Dice; +import com.urbanspork.test.TestUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Shadowsocks - Session") +class SessionTestCase { + @Test + void testGetterAndSetter() { + byte[] salt = Dice.rollBytes(32); + byte[] requestSalt = Dice.rollBytes(32); + Session session = new Session(salt, null); + TestUtil.testGetterAndSetter(requestSalt, session, Session::getRequestSalt, Session::setRequestSalt); + Assertions.assertArrayEquals(salt, session.salt()); + Assertions.assertArrayEquals(requestSalt, session.getRequestSalt()); + } +} diff --git a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPAuthCacheTestCase.java b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPAuthCacheTestCase.java index ed96282..ec52271 100644 --- a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPAuthCacheTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPAuthCacheTestCase.java @@ -3,7 +3,6 @@ import com.urbanspork.common.codec.CipherKind; import com.urbanspork.common.codec.aead.CipherMethod; import com.urbanspork.common.codec.aead.CipherMethods; -import com.urbanspork.common.codec.shadowsocks.UdpCipher; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -20,7 +19,7 @@ void testDuration() { CipherMethod method = CipherMethods.AES_GCM.get(); byte[] key = new byte[]{1}; long sessionId = 1; - UDPCipherCache cache = new UDPCipherCache(Duration.ofSeconds(1), 2); + UdpCipherCache cache = new UdpCipherCache(Duration.ofSeconds(1), 2); UdpCipher auth1 = cache.computeIfAbsent(kind, method, key, sessionId); UdpCipher auth2 = cache.computeIfAbsent(kind, method, key, sessionId); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(3)); @@ -34,7 +33,7 @@ void testLimit() { CipherKind kind = CipherKind.aead2022_blake3_aes_128_gcm; CipherMethod method = CipherMethods.AES_GCM.get(); byte[] key = new byte[]{1}; - UDPCipherCache cache = new UDPCipherCache(Duration.ofHours(1), 2); + UdpCipherCache cache = new UdpCipherCache(Duration.ofHours(1), 2); cache.computeIfAbsent(kind, method, key, 1); cache.computeIfAbsent(kind, method, key, 2); cache.computeIfAbsent(kind, method, key, 3); diff --git a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCacheTest.java b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCacheTest.java index b5024bb..e8d5c75 100644 --- a/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCacheTest.java +++ b/urban-spork-test/test/com/urbanspork/common/protocol/shadowsocks/aead2022/UDPCipherCacheTest.java @@ -12,12 +12,12 @@ class UDPCipherCacheTest { void testKey() { CipherKind kind = CipherKind.aead2022_blake3_aes_128_gcm; byte[] key = new byte[]{1}; - UDPCipherCache.Key k1 = new UDPCipherCache.Key(kind, key, 1); - UDPCipherCache.Key k2 = new UDPCipherCache.Key(kind, key, 1); + UdpCipherCache.Key k1 = new UdpCipherCache.Key(kind, key, 1); + UdpCipherCache.Key k2 = new UdpCipherCache.Key(kind, key, 1); TestUtil.testEqualsAndHashcode(k1, k2); - UDPCipherCache.Key k3 = new UDPCipherCache.Key(CipherKind.aead2022_blake3_aes_256_gcm, key, 1); - UDPCipherCache.Key k4 = new UDPCipherCache.Key(kind, new byte[]{2}, 1); - UDPCipherCache.Key k5 = new UDPCipherCache.Key(kind, key, 2); + UdpCipherCache.Key k3 = new UdpCipherCache.Key(CipherKind.aead2022_blake3_aes_256_gcm, key, 1); + UdpCipherCache.Key k4 = new UdpCipherCache.Key(kind, new byte[]{2}, 1); + UdpCipherCache.Key k5 = new UdpCipherCache.Key(kind, key, 2); Assertions.assertNotEquals(k1, k3); Assertions.assertNotEquals(k1, k4); Assertions.assertNotEquals(k1, k5); diff --git a/urban-spork-test/test/com/urbanspork/test/TCPTestCase.java b/urban-spork-test/test/com/urbanspork/test/TCPTestCase.java index 9d271c6..b837a6b 100644 --- a/urban-spork-test/test/com/urbanspork/test/TCPTestCase.java +++ b/urban-spork-test/test/com/urbanspork/test/TCPTestCase.java @@ -31,11 +31,9 @@ void testByParameter(Parameter parameter) throws ExecutionException, Interrupted serverConfig.setProtocol(protocol); serverConfig.setCipher(cipher); serverConfig.setPassword(parameter.serverPassword()); - Future server = launchServer(pool, executor, config.getServers()); - Future client = launchClient(pool, executor, config); + server = launchServer(pool, executor, config.getServers()); + client = launchClient(pool, executor, config); handshakeAndSendBytes(config); - server.cancel(true); - client.cancel(true); } void testShadowsocksAEAD2022EihByParameter(Parameter parameter) throws ExecutionException, InterruptedException { @@ -58,7 +56,6 @@ void testShadowsocksAEAD2022EihByParameter(Parameter parameter) throws Execution Future client = launchClient(pool, executor, config); handshakeAndSendBytes(config); ServerUserManager.DEFAULT.clear(); - server.cancel(true); - client.cancel(true); + cancel(client, server); } } diff --git a/urban-spork-test/test/com/urbanspork/test/TestUtil.java b/urban-spork-test/test/com/urbanspork/test/TestUtil.java index 0c737d0..26c37db 100644 --- a/urban-spork-test/test/com/urbanspork/test/TestUtil.java +++ b/urban-spork-test/test/com/urbanspork/test/TestUtil.java @@ -13,6 +13,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.BiConsumer; +import java.util.function.Function; public class TestUtil { @@ -54,7 +56,6 @@ private static boolean isBindPort(int port, Network network) { } } - public static void testEqualsAndHashcode(T t1, T t2) { Set set = new HashSet<>(); set.add(t1); @@ -68,4 +69,10 @@ public static void testEqualsAndHashcode(T t1, T t2) { Assertions.assertNotEquals(t1, map.get(t2)); Assertions.assertNotEquals(t1, new Object()); } + + public static void testGetterAndSetter(U u, T t, Function getter, BiConsumer setter) { + Assertions.assertNotEquals(u, getter.apply(t)); + setter.accept(t, u); + Assertions.assertEquals(u, getter.apply(t)); + } } diff --git a/urban-spork-test/test/com/urbanspork/test/UDPTestCase.java b/urban-spork-test/test/com/urbanspork/test/UDPTestCase.java index 5557798..3742f9b 100644 --- a/urban-spork-test/test/com/urbanspork/test/UDPTestCase.java +++ b/urban-spork-test/test/com/urbanspork/test/UDPTestCase.java @@ -34,13 +34,11 @@ void testByParameter(Parameter parameter) throws ExecutionException, Interrupted serverConfig.setProtocol(protocol); serverConfig.setCipher(cipher); serverConfig.setPassword(parameter.serverPassword()); - Future client = launchClient(service, executor, config); - Future server = launchServer(service, executor, config.getServers()); + client = launchClient(service, executor, config); + server = launchServer(service, executor, config.getServers()); for (int dstPort : dstPorts()) { handshakeAndSendBytes(config, dstPort); } - client.cancel(true); - server.cancel(true); } void testShadowsocksAEAD2022EihByParameter(Parameter parameter) throws ExecutionException, InterruptedException { @@ -68,7 +66,6 @@ void testShadowsocksAEAD2022EihByParameter(Parameter parameter) throws Execution handshakeAndSendBytes(clientConfig, dstPort); } ServerUserManager.DEFAULT.clear(); - server.cancel(true); - client.cancel(true); + cancel(client, server); } } diff --git a/urban-spork-test/test/com/urbanspork/test/template/TCPTestTemplate.java b/urban-spork-test/test/com/urbanspork/test/template/TCPTestTemplate.java index eef2820..2b3c0df 100644 --- a/urban-spork-test/test/com/urbanspork/test/template/TCPTestTemplate.java +++ b/urban-spork-test/test/com/urbanspork/test/template/TCPTestTemplate.java @@ -13,10 +13,7 @@ import io.netty.handler.codec.FixedLengthFrameDecoder; import io.netty.handler.codec.socksx.v5.Socks5CommandType; import io.netty.util.concurrent.Promise; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; import java.net.InetSocketAddress; import java.util.Arrays; @@ -28,6 +25,8 @@ public abstract class TCPTestTemplate extends TestTemplate { protected final DefaultEventLoop executor = new DefaultEventLoop(); protected final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); final int dstPort = TestUtil.freePort(); + protected Future server; + protected Future client; @BeforeAll protected void beforeAll() throws ExecutionException, InterruptedException { @@ -71,6 +70,11 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { Assertions.assertTrue(promise.isSuccess(), promise.cause() != null ? promise.cause().getMessage() : ""); } + @AfterEach + protected void afterEach() { + cancel(client, server); + } + @AfterAll protected void afterAll() { pool.shutdown(); diff --git a/urban-spork-test/test/com/urbanspork/test/template/TestTemplate.java b/urban-spork-test/test/com/urbanspork/test/template/TestTemplate.java index 64b3210..fb958e6 100644 --- a/urban-spork-test/test/com/urbanspork/test/template/TestTemplate.java +++ b/urban-spork-test/test/com/urbanspork/test/template/TestTemplate.java @@ -29,4 +29,13 @@ protected static Future launchServer(ExecutorService service, EventExecutor e Assertions.assertEquals(configs.getFirst().getPort(), promise.await().get().getFirst().localAddress().getPort()); return future; } + + protected static void cancel(Future client, Future server) { + server.cancel(true); + client.cancel(true); + boolean cancel; + do { + cancel = server.isCancelled() && client.isCancelled(); + } while (!cancel); + } } diff --git a/urban-spork-test/test/com/urbanspork/test/template/UDPTestTemplate.java b/urban-spork-test/test/com/urbanspork/test/template/UDPTestTemplate.java index d20de3f..b2881e6 100644 --- a/urban-spork-test/test/com/urbanspork/test/template/UDPTestTemplate.java +++ b/urban-spork-test/test/com/urbanspork/test/template/UDPTestTemplate.java @@ -41,6 +41,8 @@ public abstract class UDPTestTemplate extends TestTemplate { private Consumer consumer; private Future simpleEchoTestServer; private Future delayedEchoTestServer; + protected Future server; + protected Future client; @BeforeAll protected void beforeAll() { @@ -91,6 +93,11 @@ protected void handshakeAndSendBytes(ClientConfig config, int dstPort) throws In Assertions.assertTrue(promise.await(DelayedEchoTestServer.MAX_DELAYED_SECOND + 3, TimeUnit.SECONDS)); } + @AfterEach + void cancel() { + cancel(client, server); + } + @AfterAll void shutdown() { channel.close();