Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions socks5/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package socks5

// Protocol version.
const (
SocksVersion = 5
)

// Command codes (CMD) for client requests.
const (
CmdConnect = 1
CmdBind = 2
CmdUDPAssociate = 3
CmdResolve = 0xF0
CmdResolvePTR = 0xF1
)

// Address types (ATYP) used in requests and responses.
const (
AddrTypeIPv4 = 1
AddrTypeDomain = 3
AddrTypeIPv6 = 4
)

// Reply codes (REP) for server responses.
const (
RepSuccess = 0
RepGeneralFailure = 1
RepConnectionNotAllowed = 2
RepNetworkUnreachable = 3
RepHostUnreachable = 4
RepConnectionRefused = 5
RepTTLExpired = 6
RepCommandNotSupported = 7
RepAddrTypeNotSupported = 8
)

// Authentication methods (METHOD) for initial greeting.
const (
MethodNoAuth = 0x00
MethodGSSAPI = 0x01
MethodUserPass = 0x02
MethodNoAcceptable = 0xFF
)

// Authentication sub-negotiation versions.
const (
AuthVersionUserPass = 1
)

// GSS-API message types (MTYP)
const (
GSSAPITypeInit = 0x01
GSSAPITypeReply = 0x02
GSSAPITypeAbort = 0xFF
)

// GSS-API protocol version. (VER)
const (
GSSAPIVersion = 1
)
116 changes: 116 additions & 0 deletions socks5/gssapi_reply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package socks5

import (
"encoding/binary"
"errors"
"fmt"
"io"
)

// Errors for GSSAPI authentication replies.
var (
ErrInvalidGSSAPIReplyVersion = errors.New("invalid GSSAPI reply version (must be 1)")
ErrEmptyGSSAPIReplyToken = errors.New("GSSAPI reply token cannot be empty")
ErrGSSAPIReplyTooLong = errors.New("GSSAPI reply token too long (max 65535)")
)

// GSSAPIReply represents a GSSAPI authentication reply message (RFC 1961 §3.7).
type GSSAPIReply struct {
Version byte // VER (should always be 0x01)
MsgType byte // MTYP (0x02 = reply token, 0xFF = failure)
Token []byte // TOKEN (optional; none if MTYP=0xFF)
}

// Init initializes the GSSAPI reply.
func (r *GSSAPIReply) Init(version, msgType byte, token []byte) {
r.Version = version
r.MsgType = msgType
r.Token = token
}

// Validate checks for protocol correctness.
func (r *GSSAPIReply) Validate() error {
if r.Version != 0x01 {
return ErrInvalidGSSAPIReplyVersion
}
if r.MsgType == GSSAPITypeAbort {
return nil
}
if len(r.Token) == 0 {
return ErrEmptyGSSAPIReplyToken
}
if len(r.Token) > 65535 {
return ErrGSSAPIReplyTooLong
}
return nil
}

// ReadFrom reads a GSSAPI reply from a reader.
func (r *GSSAPIReply) ReadFrom(src io.Reader) (int64, error) {
var hdr [4]byte
n, err := io.ReadFull(src, hdr[:2])
if err != nil {
return int64(n), err
}

r.Version = hdr[0]
r.MsgType = hdr[1]
if r.MsgType == GSSAPITypeAbort {
return int64(n), nil
}

n2, err := io.ReadFull(src, hdr[2:4])
n += n2
if err != nil {
return int64(n), err
}
length := binary.BigEndian.Uint16(hdr[2:4])
if length == 0 {
return int64(n), ErrEmptyGSSAPIReplyToken
}

token := make([]byte, length)
n3, err := io.ReadFull(src, token)
total := int64(n + n3)
if err != nil {
return total, err
}
r.Token = token
return total, r.Validate()
}

// WriteTo writes the GSSAPI reply to a writer.
func (r *GSSAPIReply) WriteTo(dst io.Writer) (int64, error) {
if err := r.Validate(); err != nil {
return 0, err
}

if r.MsgType == GSSAPITypeAbort {
buf := [2]byte{r.Version, r.MsgType}
n, err := dst.Write(buf[:])
return int64(n), err
}

var hdr [4]byte
hdr[0] = r.Version
hdr[1] = r.MsgType
binary.BigEndian.PutUint16(hdr[2:], uint16(len(r.Token)))

n, err := dst.Write(hdr[:])
total := int64(n)
if err != nil {
return total, err
}

n2, err := dst.Write(r.Token)
total += int64(n2)
return total, err
}

// String returns a human-readable representation.
func (r *GSSAPIReply) String() string {
return fmt.Sprintf(
"GSSAPIReply{Version=%d, MsgType=0x%02x, TokenLen=%d}",
r.Version, r.MsgType, len(r.Token),
)
}
138 changes: 138 additions & 0 deletions socks5/gssapi_reply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package socks5_test

import (
"bytes"
"errors"
"io"
"testing"

"github.com/33TU/socks/socks5"
)

func Test_GSSAPIReply_Init_And_Validate(t *testing.T) {
r := &socks5.GSSAPIReply{}
r.Init(socks5.GSSAPIVersion, socks5.GSSAPITypeReply, []byte{0xca, 0xfe, 0xba, 0xbe})

if err := r.Validate(); err != nil {
t.Fatalf("expected valid reply, got %v", err)
}

r.Version = 0x02
if err := r.Validate(); !errors.Is(err, socks5.ErrInvalidGSSAPIReplyVersion) {
t.Errorf("expected ErrInvalidGSSAPIReplyVersion, got %v", err)
}

// Empty token (non-abort)
r.Version = socks5.GSSAPIVersion
r.MsgType = socks5.GSSAPITypeReply
r.Token = nil
if err := r.Validate(); !errors.Is(err, socks5.ErrEmptyGSSAPIReplyToken) {
t.Errorf("expected ErrEmptyGSSAPIReplyToken, got %v", err)
}

// Abort message (should skip token validation)
r.MsgType = socks5.GSSAPITypeAbort
r.Token = nil
if err := r.Validate(); err != nil {
t.Errorf("abort message should be valid, got %v", err)
}

// Token too long
r.MsgType = socks5.GSSAPITypeReply
r.Token = make([]byte, 70000)
if err := r.Validate(); !errors.Is(err, socks5.ErrGSSAPIReplyTooLong) {
t.Errorf("expected ErrGSSAPIReplyTooLong, got %v", err)
}
}

func Test_GSSAPIReply_WriteTo_ReadFrom_RoundTrip(t *testing.T) {
orig := &socks5.GSSAPIReply{}
orig.Init(socks5.GSSAPIVersion, socks5.GSSAPITypeReply, []byte{0xde, 0xad, 0xbe, 0xef})

var buf bytes.Buffer
n1, err := orig.WriteTo(&buf)
if err != nil {
t.Fatalf("WriteTo failed: %v", err)
}

var parsed socks5.GSSAPIReply
n2, err := parsed.ReadFrom(&buf)
if err != nil {
t.Fatalf("ReadFrom failed: %v", err)
}

if n1 != n2 {
t.Errorf("expected %d bytes read, got %d", n1, n2)
}
if parsed.Version != socks5.GSSAPIVersion {
t.Errorf("expected version 1, got %d", parsed.Version)
}
if parsed.MsgType != socks5.GSSAPITypeReply {
t.Errorf("expected msgType 0x02, got %#02x", parsed.MsgType)
}
if !bytes.Equal(parsed.Token, orig.Token) {
t.Errorf("token mismatch: got %x, want %x", parsed.Token, orig.Token)
}
}

func Test_GSSAPIReply_ReadFrom_Truncated(t *testing.T) {
data := []byte{
socks5.GSSAPIVersion, socks5.GSSAPITypeReply, 0x00, 0x04, // header: ver, mtyp, len=4
0xde, 0xad, // incomplete token
}
r := &socks5.GSSAPIReply{}
if _, err := r.ReadFrom(bytes.NewReader(data)); err == nil {
t.Errorf("expected error for truncated payload")
}
}

func Test_GSSAPIReply_ReadFrom_Abort(t *testing.T) {
data := []byte{socks5.GSSAPIVersion, socks5.GSSAPITypeAbort}
r := &socks5.GSSAPIReply{}
n, err := r.ReadFrom(bytes.NewReader(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n != 2 {
t.Errorf("expected 2 bytes read, got %d", n)
}
if r.MsgType != socks5.GSSAPITypeAbort {
t.Errorf("expected abort msgType 0xFF, got %#02x", r.MsgType)
}
}

func Test_GSSAPIReply_ReadFrom_EmptyOrTooLong(t *testing.T) {
// empty token (len=0)
data := []byte{socks5.GSSAPIVersion, socks5.GSSAPITypeReply, 0x00, 0x00}
r := &socks5.GSSAPIReply{}
if _, err := r.ReadFrom(bytes.NewReader(data)); !errors.Is(err, socks5.ErrEmptyGSSAPIReplyToken) {
t.Errorf("expected ErrEmptyGSSAPIReplyToken, got %v", err)
}

// invalid version
data = []byte{0x05, socks5.GSSAPITypeReply, 0x00, 0x01, 0xff}
if _, err := r.ReadFrom(bytes.NewReader(data)); !errors.Is(err, socks5.ErrInvalidGSSAPIReplyVersion) && err != nil {
t.Errorf("expected ErrInvalidGSSAPIReplyVersion, got %v", err)
}
}

func Test_GSSAPIReply_WriteTo_ErrorPropagation(t *testing.T) {
r := &socks5.GSSAPIReply{}
r.Init(socks5.GSSAPIVersion, socks5.GSSAPITypeReply, []byte{0xaa, 0xbb})

failWriter := writerFunc(func(p []byte) (int, error) {
return 0, io.ErrClosedPipe
})

if _, err := r.WriteTo(failWriter); err == nil {
t.Errorf("expected write error")
}
}

func Test_GSSAPIReply_String(t *testing.T) {
r := &socks5.GSSAPIReply{}
r.Init(socks5.GSSAPIVersion, socks5.GSSAPITypeReply, []byte{0xde, 0xad})
if s := r.String(); s == "" {
t.Errorf("expected non-empty String() output")
}
}
Loading