diff --git a/cmd/config.go b/cmd/config.go
index 46f5a6f..9a804e8 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -52,6 +52,11 @@ type serverConfig struct {
 	DisableMTUDiscovery bool   `json:"disable_mtu_discovery"`
 	IPv6Only            bool   `json:"ipv6_only"`
 	Resolver            string `json:"resolver"`
+	SOCKS5Outbound      struct {
+		Server   string `json:"server"`
+		User     string `json:"user"`
+		Password string `json:"password"`
+	} `json:"socks5_outbound"`
 }
 
 func (c *serverConfig) Check() error {
diff --git a/cmd/server.go b/cmd/server.go
index a47fc2b..cd6e096 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -144,6 +144,17 @@ func server(config *serverConfig) {
 	if config.IPv6Only {
 		transport.DefaultServerTransport.IPv6Only = true
 	}
+	// SOCKS5 outbound
+	if config.SOCKS5Outbound.Server != "" {
+		ob, err := transport.NewSOCKS5Client(config.SOCKS5Outbound.Server,
+			config.SOCKS5Outbound.User, config.SOCKS5Outbound.Password, 10*time.Second)
+		if err != nil {
+			logrus.WithFields(logrus.Fields{
+				"error": err,
+			}).Fatal("Failed to initialize SOCKS5 outbound")
+		}
+		transport.DefaultServerTransport.SOCKS5Client = ob
+	}
 	// ACL
 	var aclEngine *acl.Engine
 	if len(config.ACL) > 0 {
diff --git a/pkg/core/server_client.go b/pkg/core/server_client.go
index 5b3c26c..eff83e2 100644
--- a/pkg/core/server_client.go
+++ b/pkg/core/server_client.go
@@ -35,12 +35,12 @@ type serverClient struct {
 	ConnGauge              prometheus.Gauge
 
 	udpSessionMutex  sync.RWMutex
-	udpSessionMap    map[uint32]*net.UDPConn
+	udpSessionMap    map[uint32]transport.PUDPConn
 	nextUDPSessionID uint32
 	udpDefragger     defragger
 }
 
-func newServerClient(v2 bool, cs quic.Session, transport *transport.ServerTransport, auth []byte, disableUDP bool, ACLEngine *acl.Engine,
+func newServerClient(v2 bool, cs quic.Session, tr *transport.ServerTransport, auth []byte, disableUDP bool, ACLEngine *acl.Engine,
 	CTCPRequestFunc TCPRequestFunc, CTCPErrorFunc TCPErrorFunc,
 	CUDPRequestFunc UDPRequestFunc, CUDPErrorFunc UDPErrorFunc,
 	UpCounterVec, DownCounterVec *prometheus.CounterVec,
@@ -48,7 +48,7 @@ func newServerClient(v2 bool, cs quic.Session, transport *transport.ServerTransp
 	sc := &serverClient{
 		V2:              v2,
 		CS:              cs,
-		Transport:       transport,
+		Transport:       tr,
 		Auth:            auth,
 		ClientAddr:      cs.RemoteAddr(),
 		DisableUDP:      disableUDP,
@@ -57,7 +57,7 @@ func newServerClient(v2 bool, cs quic.Session, transport *transport.ServerTransp
 		CTCPErrorFunc:   CTCPErrorFunc,
 		CUDPRequestFunc: CUDPRequestFunc,
 		CUDPErrorFunc:   CUDPErrorFunc,
-		udpSessionMap:   make(map[uint32]*net.UDPConn),
+		udpSessionMap:   make(map[uint32]transport.PUDPConn),
 	}
 	if UpCounterVec != nil && DownCounterVec != nil && ConnGaugeVec != nil {
 		authB64 := base64.StdEncoding.EncodeToString(auth)
@@ -365,6 +365,7 @@ func (c *serverClient) handleUDP(stream quic.Stream) {
 				break
 			}
 		}
+		_ = stream.Close()
 	}()
 
 	// Hold the stream until it's closed by the client
diff --git a/pkg/socks5/client.go b/pkg/socks5/client.go
deleted file mode 100644
index e8ea466..0000000
--- a/pkg/socks5/client.go
+++ /dev/null
@@ -1,118 +0,0 @@
-package socks5
-
-import (
-	"encoding/binary"
-	"errors"
-	"fmt"
-	"github.com/txthinking/socks5"
-	"net"
-	"time"
-)
-
-type Client struct {
-	ServerTCPAddr *net.TCPAddr
-	Username      string
-	Password      string
-	NegTimeout    time.Duration
-}
-
-func NewClient(serverAddr string, username string, password string, negTimeout time.Duration) (*Client, error) {
-	tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr)
-	if err != nil {
-		return nil, err
-	}
-	return &Client{
-		ServerTCPAddr: tcpAddr,
-		Username:      username,
-		Password:      password,
-		NegTimeout:    negTimeout,
-	}, nil
-}
-
-func (c *Client) negotiate(conn *net.TCPConn) error {
-	m := []byte{socks5.MethodNone}
-	if c.Username != "" && c.Password != "" {
-		m = append(m, socks5.MethodUsernamePassword)
-	}
-	rq := socks5.NewNegotiationRequest(m)
-	_, err := rq.WriteTo(conn)
-	if err != nil {
-		return err
-	}
-	rs, err := socks5.NewNegotiationReplyFrom(conn)
-	if err != nil {
-		return err
-	}
-	if rs.Method == socks5.MethodUsernamePassword {
-		urq := socks5.NewUserPassNegotiationRequest([]byte(c.Username), []byte(c.Password))
-		_, err = urq.WriteTo(conn)
-		if err != nil {
-			return err
-		}
-		urs, err := socks5.NewUserPassNegotiationReplyFrom(conn)
-		if err != nil {
-			return err
-		}
-		if urs.Status != socks5.UserPassStatusSuccess {
-			return ErrUserPassAuth
-		}
-	} else if rs.Method != socks5.MethodNone {
-		return errors.New("unsupported auth method")
-	}
-	return nil
-}
-
-func (c *Client) Request(conn *net.TCPConn, r *socks5.Request) (*socks5.Reply, error) {
-	if _, err := r.WriteTo(conn); err != nil {
-		return nil, err
-	}
-	reply, err := socks5.NewReplyFrom(conn)
-	if err != nil {
-		return nil, err
-	}
-	return reply, nil
-}
-
-func (c *Client) DialTCP(raddr *net.TCPAddr) (*net.TCPConn, error) {
-	conn, err := net.DialTCP("tcp", nil, c.ServerTCPAddr)
-	if err != nil {
-		return nil, err
-	}
-	if err := conn.SetDeadline(time.Now().Add(c.NegTimeout)); err != nil {
-		return nil, err
-	}
-	err = c.negotiate(conn)
-	if err != nil {
-		_ = conn.Close()
-		return nil, err
-	}
-	var atyp byte
-	var addr, port []byte
-	if ip4 := raddr.IP.To4(); ip4 != nil {
-		atyp = socks5.ATYPIPv4
-		addr = ip4
-	} else if ip6 := raddr.IP.To16(); ip6 != nil {
-		atyp = socks5.ATYPIPv6
-		addr = ip6
-	} else {
-		_ = conn.Close()
-		return nil, errors.New("unsupported address type")
-	}
-	port = make([]byte, 2)
-	binary.BigEndian.PutUint16(port, uint16(raddr.Port))
-	r := socks5.NewRequest(socks5.CmdConnect, atyp, addr, port)
-	reply, err := c.Request(conn, r)
-	if err != nil {
-		_ = conn.Close()
-		return nil, err
-	}
-	if reply.Rep != socks5.RepSuccess {
-		_ = conn.Close()
-		return nil, fmt.Errorf("request failed: %s", reply.Rep)
-	}
-	// Negotiation succeed, disable timeout
-	if err := conn.SetDeadline(time.Time{}); err != nil {
-		return nil, err
-	}
-	return conn, nil
-}
diff --git a/pkg/transport/server.go b/pkg/transport/server.go
index fc003c4..56bc451 100644
--- a/pkg/transport/server.go
+++ b/pkg/transport/server.go
@@ -13,8 +13,15 @@ import (
 )
 
 type ServerTransport struct {
-	Dialer   *net.Dialer
-	IPv6Only bool
+	Dialer       *net.Dialer
+	IPv6Only     bool
+	SOCKS5Client *SOCKS5Client
+}
+
+type PUDPConn interface {
+	ReadFromUDP([]byte) (int, *net.UDPAddr, error)
+	WriteToUDP([]byte, *net.UDPAddr) (int, error)
+	Close() error
 }
 
 var DefaultServerTransport = &ServerTransport{
@@ -93,13 +100,21 @@ func (ct *ServerTransport) ResolveIPAddr(address string) (*net.IPAddr, error) {
 }
 
 func (ct *ServerTransport) DialTCP(raddr *net.TCPAddr) (*net.TCPConn, error) {
-	conn, err := ct.Dialer.Dial("tcp", raddr.String())
-	if err != nil {
-		return nil, err
+	if ct.SOCKS5Client != nil {
+		return ct.SOCKS5Client.DialTCP(raddr)
+	} else {
+		conn, err := ct.Dialer.Dial("tcp", raddr.String())
+		if err != nil {
+			return nil, err
+		}
+		return conn.(*net.TCPConn), nil
 	}
-	return conn.(*net.TCPConn), nil
 }
 
-func (ct *ServerTransport) ListenUDP() (*net.UDPConn, error) {
-	return net.ListenUDP("udp", nil)
+func (ct *ServerTransport) ListenUDP() (PUDPConn, error) {
+	if ct.SOCKS5Client != nil {
+		return ct.SOCKS5Client.ListenUDP()
+	} else {
+		return net.ListenUDP("udp", nil)
+	}
 }
diff --git a/pkg/transport/socks5.go b/pkg/transport/socks5.go
new file mode 100644
index 0000000..e4e64ae
--- /dev/null
+++ b/pkg/transport/socks5.go
@@ -0,0 +1,265 @@
+package transport
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"github.com/txthinking/socks5"
+	"net"
+	"time"
+)
+
+type SOCKS5Client struct {
+	ServerTCPAddr *net.TCPAddr
+	Username      string
+	Password      string
+	NegTimeout    time.Duration
+}
+
+func NewSOCKS5Client(serverAddr string, username string, password string, negTimeout time.Duration) (*SOCKS5Client, error) {
+	tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr)
+	if err != nil {
+		return nil, err
+	}
+	return &SOCKS5Client{
+		ServerTCPAddr: tcpAddr,
+		Username:      username,
+		Password:      password,
+		NegTimeout:    negTimeout,
+	}, nil
+}
+
+func (c *SOCKS5Client) negotiate(conn *net.TCPConn) error {
+	m := []byte{socks5.MethodNone}
+	if c.Username != "" && c.Password != "" {
+		m = append(m, socks5.MethodUsernamePassword)
+	}
+	rq := socks5.NewNegotiationRequest(m)
+	_, err := rq.WriteTo(conn)
+	if err != nil {
+		return err
+	}
+	rs, err := socks5.NewNegotiationReplyFrom(conn)
+	if err != nil {
+		return err
+	}
+	if rs.Method == socks5.MethodUsernamePassword {
+		urq := socks5.NewUserPassNegotiationRequest([]byte(c.Username), []byte(c.Password))
+		_, err = urq.WriteTo(conn)
+		if err != nil {
+			return err
+		}
+		urs, err := socks5.NewUserPassNegotiationReplyFrom(conn)
+		if err != nil {
+			return err
+		}
+		if urs.Status != socks5.UserPassStatusSuccess {
+			return errors.New("username or password error")
+		}
+	} else if rs.Method != socks5.MethodNone {
+		return errors.New("unsupported auth method")
+	}
+	return nil
+}
+
+func (c *SOCKS5Client) request(conn *net.TCPConn, r *socks5.Request) (*socks5.Reply, error) {
+	if _, err := r.WriteTo(conn); err != nil {
+		return nil, err
+	}
+	reply, err := socks5.NewReplyFrom(conn)
+	if err != nil {
+		return nil, err
+	}
+	return reply, nil
+}
+
+func (c *SOCKS5Client) DialTCP(raddr *net.TCPAddr) (*net.TCPConn, error) {
+	conn, err := net.DialTCP("tcp", nil, c.ServerTCPAddr)
+	if err != nil {
+		return nil, err
+	}
+	if err := conn.SetDeadline(time.Now().Add(c.NegTimeout)); err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	err = c.negotiate(conn)
+	if err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	var atyp byte
+	var addr, port []byte
+	if ip4 := raddr.IP.To4(); ip4 != nil {
+		atyp = socks5.ATYPIPv4
+		addr = ip4
+	} else if ip6 := raddr.IP.To16(); ip6 != nil {
+		atyp = socks5.ATYPIPv6
+		addr = ip6
+	} else {
+		_ = conn.Close()
+		return nil, errors.New("unsupported address type")
+	}
+	port = make([]byte, 2)
+	binary.BigEndian.PutUint16(port, uint16(raddr.Port))
+	r := socks5.NewRequest(socks5.CmdConnect, atyp, addr, port)
+	reply, err := c.request(conn, r)
+	if err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	if reply.Rep != socks5.RepSuccess {
+		_ = conn.Close()
+		return nil, fmt.Errorf("request failed: %d", reply.Rep)
+	}
+	// Negotiation succeed, disable timeout
+	if err := conn.SetDeadline(time.Time{}); err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	return conn, nil
+}
+
+func (c *SOCKS5Client) ListenUDP() (*socks5UDPConn, error) {
+	conn, err := net.DialTCP("tcp", nil, c.ServerTCPAddr)
+	if err != nil {
+		return nil, err
+	}
+	if err := conn.SetDeadline(time.Now().Add(c.NegTimeout)); err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	err = c.negotiate(conn)
+	if err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	r := socks5.NewRequest(socks5.CmdUDP, socks5.ATYPIPv4, nil, nil)
+	reply, err := c.request(conn, r)
+	if err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	if reply.Rep != socks5.RepSuccess {
+		_ = conn.Close()
+		return nil, fmt.Errorf("request failed: %d", reply.Rep)
+	}
+	// Negotiation succeed, disable timeout
+	if err := conn.SetDeadline(time.Time{}); err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	udpRelayAddr, err := socks5AddrToUDPAddr(reply.Atyp, reply.BndAddr, reply.BndPort)
+	if err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	udpConn, err := net.DialUDP("udp", nil, udpRelayAddr)
+	if err != nil {
+		_ = conn.Close()
+		return nil, err
+	}
+	sc := &socks5UDPConn{
+		tcpConn: conn,
+		udpConn: udpConn,
+	}
+	go sc.hold()
+	return sc, nil
+}
+
+type socks5UDPConn struct {
+	tcpConn *net.TCPConn
+	udpConn *net.UDPConn
+}
+
+func (c *socks5UDPConn) hold() {
+	buf := make([]byte, 1024)
+	for {
+		_, err := c.tcpConn.Read(buf)
+		if err != nil {
+			break
+		}
+	}
+	_ = c.tcpConn.Close()
+	_ = c.udpConn.Close()
+}
+
+func (c *socks5UDPConn) ReadFromUDP(b []byte) (int, *net.UDPAddr, error) {
+	n, err := c.udpConn.Read(b)
+	if err != nil {
+		return 0, nil, err
+	}
+	d, err := socks5.NewDatagramFromBytes(b[:n])
+	if err != nil {
+		return 0, nil, err
+	}
+	addr, err := socks5AddrToUDPAddr(d.Atyp, d.DstAddr, d.DstPort)
+	if err != nil {
+		return 0, nil, err
+	}
+	n = copy(b, d.Data)
+	return n, addr, nil
+}
+
+func (c *socks5UDPConn) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) {
+	var atyp byte
+	var dstAddr, dstPort []byte
+	if ip4 := addr.IP.To4(); ip4 != nil {
+		atyp = socks5.ATYPIPv4
+		dstAddr = ip4
+	} else if ip6 := addr.IP.To16(); ip6 != nil {
+		atyp = socks5.ATYPIPv6
+		dstAddr = ip6
+	} else {
+		return 0, errors.New("unsupported address type")
+	}
+	dstPort = make([]byte, 2)
+	binary.BigEndian.PutUint16(dstPort, uint16(addr.Port))
+	d := socks5.NewDatagram(atyp, dstAddr, dstPort, b)
+	_, err := c.udpConn.Write(d.Bytes())
+	if err != nil {
+		return 0, err
+	}
+	return len(b), nil
+}
+
+func (c *socks5UDPConn) Close() error {
+	_ = c.tcpConn.Close()
+	_ = c.udpConn.Close()
+	return nil
+}
+
+func socks5AddrToUDPAddr(atyp byte, addr []byte, port []byte) (*net.UDPAddr, error) {
+	switch atyp {
+	case socks5.ATYPIPv4:
+		if len(addr) != 4 {
+			return nil, errors.New("invalid ipv4 address")
+		}
+		return &net.UDPAddr{
+			IP:   addr,
+			Port: int(binary.BigEndian.Uint16(port)),
+		}, nil
+	case socks5.ATYPIPv6:
+		if len(addr) != 16 {
+			return nil, errors.New("invalid ipv6 address")
+		}
+		return &net.UDPAddr{
+			IP:   addr,
+			Port: int(binary.BigEndian.Uint16(port)),
+		}, nil
+	case socks5.ATYPDomain:
+		if len(addr) == 0 {
+			return nil, errors.New("invalid domain address")
+		}
+		ipAddr, err := net.ResolveIPAddr("ip", string(addr))
+		if err != nil {
+			return nil, err
+		}
+		return &net.UDPAddr{
+			IP:   ipAddr.IP,
+			Port: int(binary.BigEndian.Uint16(port)),
+			Zone: ipAddr.Zone,
+		}, nil
+	default:
+		return nil, errors.New("unsupported address type")
+	}
+}