Skip to content

Commit 7f7822a

Browse files
committed
stream/socket states
1 parent 8ce6730 commit 7f7822a

5 files changed

Lines changed: 252 additions & 0 deletions

File tree

internal/socksproto/target.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// ==============================================================================
2+
// MasterDnsVPN
3+
// Author: MasterkinG32
4+
// Github: https://github.com/masterking32
5+
// Year: 2026
6+
// ==============================================================================
7+
8+
package socksproto
9+
10+
import (
11+
"encoding/binary"
12+
"errors"
13+
"net"
14+
)
15+
16+
const (
17+
AddressTypeIPv4 = 0x01
18+
AddressTypeDomain = 0x03
19+
AddressTypeIPv6 = 0x04
20+
)
21+
22+
var (
23+
ErrTargetTooShort = errors.New("socks target payload too short")
24+
ErrUnsupportedAddressType = errors.New("unsupported socks address type")
25+
ErrInvalidDomainLength = errors.New("invalid socks domain length")
26+
)
27+
28+
type Target struct {
29+
AddressType uint8
30+
Host string
31+
Port uint16
32+
}
33+
34+
func ParseTargetPayload(payload []byte) (Target, error) {
35+
if len(payload) < 3 {
36+
return Target{}, ErrTargetTooShort
37+
}
38+
39+
target := Target{AddressType: payload[0]}
40+
offset := 1
41+
42+
switch payload[0] {
43+
case AddressTypeIPv4:
44+
if len(payload) < offset+4+2 {
45+
return Target{}, ErrTargetTooShort
46+
}
47+
target.Host = net.IP(payload[offset : offset+4]).String()
48+
offset += 4
49+
case AddressTypeDomain:
50+
if len(payload) < offset+1 {
51+
return Target{}, ErrTargetTooShort
52+
}
53+
domainLength := int(payload[offset])
54+
offset++
55+
if domainLength < 1 || len(payload) < offset+domainLength+2 {
56+
return Target{}, ErrInvalidDomainLength
57+
}
58+
target.Host = string(payload[offset : offset+domainLength])
59+
offset += domainLength
60+
case AddressTypeIPv6:
61+
if len(payload) < offset+16+2 {
62+
return Target{}, ErrTargetTooShort
63+
}
64+
target.Host = net.IP(payload[offset : offset+16]).String()
65+
offset += 16
66+
default:
67+
return Target{}, ErrUnsupportedAddressType
68+
}
69+
70+
target.Port = binary.BigEndian.Uint16(payload[offset : offset+2])
71+
return target, nil
72+
}

internal/socksproto/target_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// ==============================================================================
2+
// MasterDnsVPN
3+
// Author: MasterkinG32
4+
// Github: https://github.com/masterking32
5+
// Year: 2026
6+
// ==============================================================================
7+
8+
package socksproto
9+
10+
import "testing"
11+
12+
func TestParseTargetPayloadIPv4(t *testing.T) {
13+
target, err := ParseTargetPayload([]byte{0x01, 127, 0, 0, 1, 0x01, 0xBB})
14+
if err != nil {
15+
t.Fatalf("ParseTargetPayload returned error: %v", err)
16+
}
17+
if target.Host != "127.0.0.1" || target.Port != 443 {
18+
t.Fatalf("unexpected target: %+v", target)
19+
}
20+
}
21+
22+
func TestParseTargetPayloadDomain(t *testing.T) {
23+
target, err := ParseTargetPayload([]byte{0x03, 0x0B, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x00, 0x35})
24+
if err != nil {
25+
t.Fatalf("ParseTargetPayload returned error: %v", err)
26+
}
27+
if target.Host != "example.com" || target.Port != 53 {
28+
t.Fatalf("unexpected target: %+v", target)
29+
}
30+
}
31+
32+
func TestParseTargetPayloadRejectsUnsupportedType(t *testing.T) {
33+
if _, err := ParseTargetPayload([]byte{0x05, 0x00, 0x35}); err != ErrUnsupportedAddressType {
34+
t.Fatalf("unexpected error: %v", err)
35+
}
36+
}

internal/udpserver/server.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
Enums "masterdnsvpn-go/internal/enums"
2626
"masterdnsvpn-go/internal/logger"
2727
"masterdnsvpn-go/internal/security"
28+
SocksProto "masterdnsvpn-go/internal/socksproto"
2829
VpnProto "masterdnsvpn-go/internal/vpnproto"
2930
)
3031

@@ -381,6 +382,8 @@ func (s *Server) handleTunnelCandidate(packet []byte, parsed DnsParser.LitePacke
381382
return s.handleDNSQueryRequest(packet, parsed, decision, vpnPacket)
382383
case Enums.PACKET_STREAM_SYN:
383384
return s.handleStreamSynRequest(packet, decision, vpnPacket)
385+
case Enums.PACKET_SOCKS5_SYN:
386+
return s.handleSOCKS5SynRequest(packet, decision, vpnPacket)
384387
case Enums.PACKET_STREAM_DATA, Enums.PACKET_STREAM_RESEND:
385388
return s.handleStreamDataRequest(packet, decision, vpnPacket)
386389
case Enums.PACKET_STREAM_FIN:
@@ -693,6 +696,54 @@ func (s *Server) handleStreamSynRequest(questionPacket []byte, decision domainma
693696
})
694697
}
695698

699+
func (s *Server) handleSOCKS5SynRequest(questionPacket []byte, decision domainmatcher.Decision, vpnPacket VpnProto.Packet) []byte {
700+
if !vpnPacket.HasStreamID || vpnPacket.StreamID == 0 || !vpnPacket.HasSequenceNum {
701+
return nil
702+
}
703+
sessionRecord, ok := s.sessions.Active(vpnPacket.SessionID)
704+
if !ok {
705+
return nil
706+
}
707+
708+
target, err := SocksProto.ParseTargetPayload(vpnPacket.Payload)
709+
if err != nil {
710+
packetType := uint8(Enums.PACKET_SOCKS5_CONNECT_FAIL)
711+
if errors.Is(err, SocksProto.ErrUnsupportedAddressType) || errors.Is(err, SocksProto.ErrInvalidDomainLength) {
712+
packetType = uint8(Enums.PACKET_SOCKS5_ADDRESS_TYPE_UNSUPPORTED)
713+
}
714+
return s.buildSessionVPNResponse(questionPacket, decision.RequestName, sessionRecord, VpnProto.Packet{
715+
PacketType: packetType,
716+
StreamID: vpnPacket.StreamID,
717+
SequenceNum: vpnPacket.SequenceNum,
718+
})
719+
}
720+
721+
record, ok := s.streams.BindTarget(vpnPacket.SessionID, vpnPacket.StreamID, target.Host, target.Port, time.Now())
722+
if !ok || record == nil {
723+
return s.buildSessionVPNResponse(questionPacket, decision.RequestName, sessionRecord, VpnProto.Packet{
724+
PacketType: Enums.PACKET_SOCKS5_CONNECT_FAIL,
725+
StreamID: vpnPacket.StreamID,
726+
SequenceNum: vpnPacket.SequenceNum,
727+
})
728+
}
729+
730+
if s.log != nil {
731+
s.log.Debugf(
732+
"🧦 <green>SOCKS5 Stream Prepared</green> <magenta>|</magenta> <blue>Session</blue>: <cyan>%d</cyan> <magenta>|</magenta> <blue>Stream</blue>: <cyan>%d</cyan> <magenta>|</magenta> <blue>Target</blue>: <cyan>%s:%d</cyan>",
733+
record.SessionID,
734+
record.StreamID,
735+
record.TargetHost,
736+
record.TargetPort,
737+
)
738+
}
739+
740+
return s.buildSessionVPNResponse(questionPacket, decision.RequestName, sessionRecord, VpnProto.Packet{
741+
PacketType: Enums.PACKET_SOCKS5_SYN_ACK,
742+
StreamID: vpnPacket.StreamID,
743+
SequenceNum: vpnPacket.SequenceNum,
744+
})
745+
}
746+
696747
func (s *Server) handleStreamDataRequest(questionPacket []byte, decision domainmatcher.Decision, vpnPacket VpnProto.Packet) []byte {
697748
if !vpnPacket.HasStreamID || vpnPacket.StreamID == 0 || !vpnPacket.HasSequenceNum {
698749
return nil

internal/udpserver/server_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,80 @@ func TestHandlePacketResetsUnknownStreamData(t *testing.T) {
540540
}
541541
}
542542

543+
func TestHandlePacketRespondsToSocks5Syn(t *testing.T) {
544+
codec, err := security.NewCodec(0, "")
545+
if err != nil {
546+
t.Fatalf("NewCodec returned error: %v", err)
547+
}
548+
549+
srv := New(config.ServerConfig{
550+
MaxPacketSize: 65535,
551+
Domain: []string{"a.com"},
552+
MinVPNLabelLength: 3,
553+
}, nil, codec)
554+
555+
initPayload := []byte{0, 0x00, 0x00, 0x96, 0x00, 0xC8, 0x10, 0x20, 0x30, 0x40}
556+
initResponse := srv.handlePacket(buildTunnelQueryWithSessionID(t, codec, "a.com", 0, Enums.PACKET_SESSION_INIT, initPayload))
557+
packet, err := DnsParser.ExtractVPNResponse(initResponse, false)
558+
if err != nil {
559+
t.Fatalf("ExtractVPNResponse returned error: %v", err)
560+
}
561+
sessionID := packet.Payload[0]
562+
sessionCookie := packet.Payload[1]
563+
564+
_ = srv.handlePacket(buildTunnelStreamQuery(t, codec, "a.com", sessionID, sessionCookie, Enums.PACKET_STREAM_SYN, 15, 1, nil))
565+
566+
targetPayload := []byte{0x03, 0x0B, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x01, 0xBB}
567+
query := buildTunnelStreamQuery(t, codec, "a.com", sessionID, sessionCookie, Enums.PACKET_SOCKS5_SYN, 15, 2, targetPayload)
568+
response := srv.handlePacket(query)
569+
vpnResponse, err := DnsParser.ExtractVPNResponse(response, false)
570+
if err != nil {
571+
t.Fatalf("ExtractVPNResponse returned error: %v", err)
572+
}
573+
if vpnResponse.PacketType != Enums.PACKET_SOCKS5_SYN_ACK {
574+
t.Fatalf("unexpected packet type: got=%d want=%d", vpnResponse.PacketType, Enums.PACKET_SOCKS5_SYN_ACK)
575+
}
576+
577+
streamRecord, ok := srv.streams.Lookup(sessionID, 15)
578+
if !ok {
579+
t.Fatal("expected stream state to exist")
580+
}
581+
if streamRecord.TargetHost != "example.com" || streamRecord.TargetPort != 443 {
582+
t.Fatalf("unexpected bound target: %+v", streamRecord)
583+
}
584+
}
585+
586+
func TestHandlePacketRejectsInvalidSocks5Syn(t *testing.T) {
587+
codec, err := security.NewCodec(0, "")
588+
if err != nil {
589+
t.Fatalf("NewCodec returned error: %v", err)
590+
}
591+
592+
srv := New(config.ServerConfig{
593+
MaxPacketSize: 65535,
594+
Domain: []string{"a.com"},
595+
MinVPNLabelLength: 3,
596+
}, nil, codec)
597+
598+
initPayload := []byte{0, 0x00, 0x00, 0x96, 0x00, 0xC8, 0x10, 0x20, 0x30, 0x40}
599+
initResponse := srv.handlePacket(buildTunnelQueryWithSessionID(t, codec, "a.com", 0, Enums.PACKET_SESSION_INIT, initPayload))
600+
packet, err := DnsParser.ExtractVPNResponse(initResponse, false)
601+
if err != nil {
602+
t.Fatalf("ExtractVPNResponse returned error: %v", err)
603+
}
604+
605+
_ = srv.handlePacket(buildTunnelStreamQuery(t, codec, "a.com", packet.Payload[0], packet.Payload[1], Enums.PACKET_STREAM_SYN, 16, 1, nil))
606+
query := buildTunnelStreamQuery(t, codec, "a.com", packet.Payload[0], packet.Payload[1], Enums.PACKET_SOCKS5_SYN, 16, 2, []byte{0x09, 0x00, 0x35})
607+
response := srv.handlePacket(query)
608+
vpnResponse, err := DnsParser.ExtractVPNResponse(response, false)
609+
if err != nil {
610+
t.Fatalf("ExtractVPNResponse returned error: %v", err)
611+
}
612+
if vpnResponse.PacketType != Enums.PACKET_SOCKS5_ADDRESS_TYPE_UNSUPPORTED {
613+
t.Fatalf("unexpected packet type: got=%d want=%d", vpnResponse.PacketType, Enums.PACKET_SOCKS5_ADDRESS_TYPE_UNSUPPORTED)
614+
}
615+
}
616+
543617
func TestSessionStoreExpiresReuseSignatureWithoutDroppingSession(t *testing.T) {
544618
store := newSessionStore()
545619
payload := []byte{1, 0x21, 0x00, 0x96, 0x00, 0xC8, 0x44, 0x33, 0x22, 0x11}

internal/udpserver/stream_state.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ type streamStateRecord struct {
1818
SessionID uint8
1919
StreamID uint16
2020
State uint8
21+
TargetHost string
22+
TargetPort uint16
2123
CreatedAt time.Time
2224
LastActivityAt time.Time
2325
LastSequence uint16
@@ -60,6 +62,23 @@ func (s *streamStateStore) EnsureOpen(sessionID uint8, streamID uint16, now time
6062
return cloneStreamStateRecord(record), true
6163
}
6264

65+
func (s *streamStateStore) BindTarget(sessionID uint8, streamID uint16, host string, port uint16, now time.Time) (*streamStateRecord, bool) {
66+
s.mu.Lock()
67+
defer s.mu.Unlock()
68+
69+
record := s.lookupLocked(sessionID, streamID)
70+
if record == nil {
71+
return nil, false
72+
}
73+
if record.TargetHost != "" && (record.TargetHost != host || record.TargetPort != port) {
74+
return nil, false
75+
}
76+
record.TargetHost = host
77+
record.TargetPort = port
78+
record.LastActivityAt = now
79+
return cloneStreamStateRecord(record), true
80+
}
81+
6382
func (s *streamStateStore) Touch(sessionID uint8, streamID uint16, sequenceNum uint16, now time.Time) (*streamStateRecord, bool) {
6483
s.mu.Lock()
6584
defer s.mu.Unlock()

0 commit comments

Comments
 (0)