Skip to content

Commit 817bab1

Browse files
authored
Add support for SOCKS5 private authentication methods (RFC#1928) (#15470)
Motivation: The SOCKS5 protocol, as defined by RFC 1928, reserves authentication method codes from 0x80 to 0xFE for private or experimental use. The current Socks5ProxyHandler in Netty only supports the standard methods: No Authentication (0x00) and GSS-API / Username/Password (0x01/0x02). This limitation prevents users from implementing and utilizing custom authentication schemes with SOCKS5 proxy servers that require private authentication methods. This change addresses this gap by adding support for these private authentication methods, as requested in issue #15460. Modification: Introduced a new constructor in Socks5ProxyHandler that accepts a byte for the private authentication method and a byte[] for the authentication token/payload. When this new constructor is used, the handler will offer the specified private method to the SOCKS5 proxy during the initial handshake. If the proxy selects the private method, the handler proceeds with the private authentication sub-negotiation by sending the provided token. Added corresponding tests in ProxyHandlerTest.java to validate the new functionality. This includes creating a test Socks5ProxyServer that supports a private authentication method and adding test cases for successful authentication, authentication failure (bad token), and connection rejection. Result: Fixes #15460. This enhancement allows users to connect to SOCKS5 proxies that require custom, non-standard authentication, increasing the flexibility and completeness of Netty's proxy support.
1 parent c4ac661 commit 817bab1

15 files changed

Lines changed: 784 additions & 14 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2025 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.codec.socksx.v5;
17+
18+
import io.netty.handler.codec.DecoderResult;
19+
import io.netty.util.internal.ObjectUtil;
20+
import io.netty.util.internal.StringUtil;
21+
22+
/**
23+
* The default {@link Socks5PrivateAuthRequest} implementation.
24+
* <p>
25+
* For custom private authentication protocols, you should implement the {@link Socks5PrivateAuthRequest}
26+
* interface directly. Custom protocols should also implement their own encoder/decoder to handle the wire format.
27+
* </p>
28+
*/
29+
public final class DefaultSocks5PrivateAuthRequest extends AbstractSocks5Message
30+
implements Socks5PrivateAuthRequest {
31+
32+
/**
33+
* The private authentication token.
34+
*/
35+
private final byte[] privateToken;
36+
37+
/**
38+
* Creates a new instance with the specified token.
39+
*
40+
* @param privateAuthToken the private authentication token
41+
*/
42+
public DefaultSocks5PrivateAuthRequest(final byte[] privateAuthToken) {
43+
this.privateToken = ObjectUtil.checkNotNull(privateAuthToken, "privateToken").clone();
44+
}
45+
46+
@Override
47+
public byte[] privateToken() {
48+
return privateToken.clone();
49+
}
50+
51+
@Override
52+
public String toString() {
53+
StringBuilder buf = new StringBuilder(StringUtil.simpleClassName(this));
54+
55+
DecoderResult decoderResult = decoderResult();
56+
if (!decoderResult.isSuccess()) {
57+
buf.append("(decoderResult: ");
58+
buf.append(decoderResult);
59+
buf.append(", privateToken: ****)");
60+
} else {
61+
buf.append("(privateToken: ****)");
62+
}
63+
64+
return buf.toString();
65+
}
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2025 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.codec.socksx.v5;
17+
18+
import io.netty.handler.codec.DecoderResult;
19+
import io.netty.util.internal.ObjectUtil;
20+
import io.netty.util.internal.StringUtil;
21+
22+
/**
23+
* The default {@link Socks5PrivateAuthResponse} implementation.
24+
*/
25+
public final class DefaultSocks5PrivateAuthResponse extends AbstractSocks5Message
26+
implements Socks5PrivateAuthResponse {
27+
28+
/**
29+
* The authentication status.
30+
*/
31+
private final Socks5PrivateAuthStatus status;
32+
33+
/**
34+
* Creates a new instance with the specified status.
35+
*
36+
* @param authStatus the authentication status
37+
*/
38+
public DefaultSocks5PrivateAuthResponse(final Socks5PrivateAuthStatus authStatus) {
39+
this.status = ObjectUtil.checkNotNull(authStatus, "authStatus");
40+
}
41+
42+
@Override
43+
public Socks5PrivateAuthStatus status() {
44+
return status;
45+
}
46+
47+
@Override
48+
public String toString() {
49+
StringBuilder buf = new StringBuilder(StringUtil.simpleClassName(this));
50+
51+
DecoderResult decoderResult = decoderResult();
52+
if (!decoderResult.isSuccess()) {
53+
buf.append("(decoderResult: ");
54+
buf.append(decoderResult);
55+
buf.append(", status: ");
56+
buf.append(status);
57+
buf.append(')');
58+
} else {
59+
buf.append("(status: ");
60+
buf.append(status);
61+
buf.append(')');
62+
}
63+
64+
return buf.toString();
65+
}
66+
}

codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ public class Socks5AuthMethod implements Comparable<Socks5AuthMethod> {
3232
*/
3333
public static final Socks5AuthMethod UNACCEPTED = new Socks5AuthMethod(0xff, "UNACCEPTED");
3434

35+
/**
36+
* Returns whether the authentication method code is in the private methods range (0x80-0xFE)
37+
* as defined by <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928 section 3</a>.
38+
*
39+
* @param b The authentication method code
40+
* @return true if the code is in the private methods range
41+
*/
42+
public static boolean isPrivateMethod(byte b) {
43+
int ubyte = b & 0xFF;
44+
return ubyte >= 0x80 && ubyte <= 0xFE;
45+
}
46+
3547
public static Socks5AuthMethod valueOf(byte b) {
3648
switch (b) {
3749
case 0x00:
@@ -44,6 +56,11 @@ public static Socks5AuthMethod valueOf(byte b) {
4456
return UNACCEPTED;
4557
}
4658

59+
// Handle all private methods (0x80-0xFE)
60+
if (isPrivateMethod(b)) {
61+
return new Socks5AuthMethod(b, "PRIVATE_" + (b & 0xFF));
62+
}
63+
4764
return new Socks5AuthMethod(b);
4865
}
4966

codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ protected void encode(ChannelHandlerContext ctx, Socks5Message msg, ByteBuf out)
6666
encodeAuthMethodRequest((Socks5InitialRequest) msg, out);
6767
} else if (msg instanceof Socks5PasswordAuthRequest) {
6868
encodePasswordAuthRequest((Socks5PasswordAuthRequest) msg, out);
69+
} else if (msg instanceof Socks5PrivateAuthRequest) {
70+
encodePrivateAuthRequest((Socks5PrivateAuthRequest) msg, out);
6971
} else if (msg instanceof Socks5CommandRequest) {
7072
encodeCommandRequest((Socks5CommandRequest) msg, out);
7173
} else {
@@ -103,6 +105,13 @@ private static void encodePasswordAuthRequest(Socks5PasswordAuthRequest msg, Byt
103105
ByteBufUtil.writeAscii(out, password);
104106
}
105107

108+
private static void encodePrivateAuthRequest(Socks5PrivateAuthRequest msg, ByteBuf out) {
109+
byte[] bytes = msg.privateToken();
110+
out.writeByte(0x01);
111+
out.writeByte(bytes.length);
112+
out.writeBytes(bytes);
113+
}
114+
106115
private void encodeCommandRequest(Socks5CommandRequest msg, ByteBuf out) throws Exception {
107116
out.writeByte(msg.version().byteValue());
108117
out.writeByte(msg.type().byteValue());
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2025 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.codec.socksx.v5;
17+
18+
/**
19+
* A SOCKS5 subnegotiation request for private authentication.
20+
* <p>
21+
* RFC 1928 reserves method codes 0x80-0xFE for private authentication methods.
22+
* This interface provides a base implementation for method 0x80, but can be extended
23+
* for other private authentication methods by implementing custom encoders/decoders.
24+
* </p>
25+
*
26+
* @see <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928 Section 3</a>
27+
*/
28+
public interface Socks5PrivateAuthRequest extends Socks5Message {
29+
30+
/**
31+
* Returns the private token of this request.
32+
* <p>
33+
* For custom subnegotiation protocols, this could be extended by adding
34+
* additional methods in a subinterface.
35+
* </p>
36+
*
37+
* @return the private authentication token
38+
*/
39+
byte[] privateToken();
40+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2025 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.codec.socksx.v5;
17+
18+
import io.netty.buffer.ByteBuf;
19+
import io.netty.channel.ChannelHandlerContext;
20+
import io.netty.handler.codec.DecoderException;
21+
import io.netty.handler.codec.DecoderResult;
22+
import io.netty.handler.codec.ByteToMessageDecoder;
23+
import io.netty.util.internal.EmptyArrays;
24+
25+
import java.util.List;
26+
27+
/**
28+
* Decodes a single {@link Socks5PrivateAuthRequest} from the inbound {@link ByteBuf}s.
29+
* On successful decode, this decoder will forward the received data to the next handler, so that
30+
* other handler can remove or replace this decoder later.
31+
*/
32+
public final class Socks5PrivateAuthRequestDecoder extends ByteToMessageDecoder {
33+
34+
private boolean decoded;
35+
36+
@Override
37+
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
38+
try {
39+
if (decoded) {
40+
int readableBytes = in.readableBytes();
41+
if (readableBytes > 0) {
42+
out.add(in.readRetainedSlice(readableBytes));
43+
}
44+
return;
45+
}
46+
47+
// Check if we have enough data to decode the message
48+
if (in.readableBytes() < 2) {
49+
return;
50+
}
51+
52+
final int startOffset = in.readerIndex();
53+
final byte version = in.getByte(startOffset);
54+
if (version != 1) {
55+
throw new DecoderException("unsupported subnegotiation version: " + version + " (expected: 1)");
56+
}
57+
58+
final int tokenLength = in.getUnsignedByte(startOffset + 1);
59+
60+
// Check if the full message is available
61+
if (in.readableBytes() < 2 + tokenLength) {
62+
return;
63+
}
64+
65+
// Read the version and token length
66+
in.skipBytes(2);
67+
68+
// Read the token
69+
byte[] token = new byte[tokenLength];
70+
in.readBytes(token);
71+
72+
// Add the decoded token to the output list
73+
out.add(new DefaultSocks5PrivateAuthRequest(token));
74+
75+
// Mark as decoded to handle remaining bytes in future calls
76+
decoded = true;
77+
} catch (Exception e) {
78+
fail(out, e);
79+
}
80+
}
81+
82+
private void fail(List<Object> out, Exception cause) {
83+
if (!(cause instanceof DecoderException)) {
84+
cause = new DecoderException(cause);
85+
}
86+
87+
decoded = true;
88+
89+
Socks5Message m = new
90+
DefaultSocks5PrivateAuthRequest(EmptyArrays.EMPTY_BYTES);
91+
m.setDecoderResult(DecoderResult.failure(cause));
92+
out.add(m);
93+
}
94+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.codec.socksx.v5;
17+
18+
/**
19+
* A SOCKS5 subnegotiation response for private authentication.
20+
* <p>
21+
* This interface corresponds to the response for private authentication methods
22+
* in the range 0x80-0xFE as defined in RFC 1928. For custom private authentication
23+
* protocols, this interface can be extended with additional methods.
24+
* </p>
25+
*
26+
* @see <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928 Section 3</a>
27+
*/
28+
public interface Socks5PrivateAuthResponse extends Socks5Message {
29+
30+
/**
31+
* Returns the status of this response.
32+
*
33+
* @return the authentication status
34+
*/
35+
Socks5PrivateAuthStatus status();
36+
}

0 commit comments

Comments
 (0)