mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-24 18:10:32 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b905381fb | ||
|
|
ea3275a77b |
@@ -5,8 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type windowsReader struct {
|
||||
bufs []syscall.WSABuf
|
||||
ready bool
|
||||
bufs []syscall.WSABuf
|
||||
}
|
||||
|
||||
func (r *windowsReader) Init(bs []*Buffer) {
|
||||
@@ -16,7 +15,6 @@ func (r *windowsReader) Init(bs []*Buffer) {
|
||||
for _, b := range bs {
|
||||
r.bufs = append(r.bufs, syscall.WSABuf{Len: uint32(Size), Buf: &b.v[0]})
|
||||
}
|
||||
r.ready = false
|
||||
}
|
||||
|
||||
func (r *windowsReader) Clear() {
|
||||
@@ -27,14 +25,6 @@ func (r *windowsReader) Clear() {
|
||||
}
|
||||
|
||||
func (r *windowsReader) Read(fd uintptr) int32 {
|
||||
// On the first invocation, we return -1 to indicate "not ready"
|
||||
// to make rawConn.Read wait for readability using the runtime's own mechanism
|
||||
// because syscall.WSARecv() is a blocking call when used with nil OVERLAPPED
|
||||
if !r.ready {
|
||||
r.ready = true
|
||||
return -1
|
||||
}
|
||||
|
||||
var nBytes uint32
|
||||
var flags uint32
|
||||
err := syscall.WSARecv(syscall.Handle(fd), &r.bufs[0], uint32(len(r.bufs)), &nBytes, &flags, nil, nil)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
func ToNetwork(network string) net.Network {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
return net.Network_TCP
|
||||
case N.NetworkUDP:
|
||||
return net.Network_UDP
|
||||
default:
|
||||
return net.Network_Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func ToDestination(socksaddr M.Socksaddr, network net.Network) (net.Destination, error) {
|
||||
// IsFqdn() implicitly checks if the domain name is valid
|
||||
if socksaddr.IsFqdn() {
|
||||
return net.Destination{
|
||||
Network: network,
|
||||
Address: net.DomainAddress(socksaddr.Fqdn),
|
||||
Port: net.Port(socksaddr.Port),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsIP() implicitly checks if the IP address is valid
|
||||
if socksaddr.IsIP() {
|
||||
return net.Destination{
|
||||
Network: network,
|
||||
Address: net.IPAddress(socksaddr.Addr.AsSlice()),
|
||||
Port: net.Port(socksaddr.Port),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return net.Destination{}, errors.New("invalid socks address: ", socksaddr)
|
||||
}
|
||||
|
||||
func ToSocksaddr(destination net.Destination) M.Socksaddr {
|
||||
var addr M.Socksaddr
|
||||
switch destination.Address.Family() {
|
||||
case net.AddressFamilyDomain:
|
||||
addr.Fqdn = destination.Address.Domain()
|
||||
default:
|
||||
addr.Addr = M.AddrFromIP(destination.Address.IP())
|
||||
}
|
||||
addr.Port = uint16(destination.Port)
|
||||
return addr
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/net/cnc"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/proxy"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/pipe"
|
||||
)
|
||||
|
||||
var _ N.Dialer = (*XrayDialer)(nil)
|
||||
|
||||
type XrayDialer struct {
|
||||
internet.Dialer
|
||||
}
|
||||
|
||||
func NewDialer(dialer internet.Dialer) *XrayDialer {
|
||||
return &XrayDialer{dialer}
|
||||
}
|
||||
|
||||
func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
dest, err := ToDestination(destination, ToNetwork(network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Dialer.Dial(ctx, dest)
|
||||
}
|
||||
|
||||
func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
type XrayOutboundDialer struct {
|
||||
outbound proxy.Outbound
|
||||
dialer internet.Dialer
|
||||
}
|
||||
|
||||
func NewOutboundDialer(outbound proxy.Outbound, dialer internet.Dialer) *XrayOutboundDialer {
|
||||
return &XrayOutboundDialer{outbound, dialer}
|
||||
}
|
||||
|
||||
func (d *XrayOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
dest, err := ToDestination(destination, ToNetwork(network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
if len(outbounds) == 0 {
|
||||
outbounds = []*session.Outbound{{}}
|
||||
ctx = session.ContextWithOutbounds(ctx, outbounds)
|
||||
}
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
ob.Target = dest
|
||||
|
||||
opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)}
|
||||
uplinkReader, uplinkWriter := pipe.New(opts...)
|
||||
downlinkReader, downlinkWriter := pipe.New(opts...)
|
||||
conn := cnc.NewConnection(cnc.ConnectionInputMulti(downlinkWriter), cnc.ConnectionOutputMulti(uplinkReader))
|
||||
go d.outbound.Process(ctx, &transport.Link{Reader: downlinkReader, Writer: uplinkWriter}, d.dialer)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (d *XrayOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
func ReturnError(err error) error {
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
_ N.TCPConnectionHandler = (*Dispatcher)(nil)
|
||||
_ N.UDPConnectionHandler = (*Dispatcher)(nil)
|
||||
)
|
||||
|
||||
type Dispatcher struct {
|
||||
upstream routing.Dispatcher
|
||||
newErrorFunc func(values ...any) *errors.Error
|
||||
}
|
||||
|
||||
func NewDispatcher(dispatcher routing.Dispatcher, newErrorFunc func(values ...any) *errors.Error) *Dispatcher {
|
||||
return &Dispatcher{
|
||||
upstream: dispatcher,
|
||||
newErrorFunc: newErrorFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
dest, err := ToDestination(metadata.Destination, net.Network_TCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
xConn := NewConn(conn)
|
||||
return d.upstream.DispatchLink(ctx, dest, &transport.Link{
|
||||
Reader: xConn,
|
||||
Writer: xConn,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Dispatcher) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
dest, err := ToDestination(metadata.Destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.upstream.DispatchLink(ctx, dest, &transport.Link{
|
||||
Reader: buf.NewPacketReader(conn.(io.Reader)),
|
||||
Writer: buf.NewWriter(conn.(io.Writer)),
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Dispatcher) NewError(ctx context.Context, err error) {
|
||||
errors.LogInfo(ctx, err.Error())
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
var _ logger.ContextLogger = (*XrayLogger)(nil)
|
||||
|
||||
type XrayLogger struct {
|
||||
newError func(values ...any) *errors.Error
|
||||
}
|
||||
|
||||
func NewLogger(newErrorFunc func(values ...any) *errors.Error) *XrayLogger {
|
||||
return &XrayLogger{
|
||||
newErrorFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *XrayLogger) Trace(args ...any) {
|
||||
}
|
||||
|
||||
func (l *XrayLogger) Debug(args ...any) {
|
||||
errors.LogDebug(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) Info(args ...any) {
|
||||
errors.LogInfo(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) Warn(args ...any) {
|
||||
errors.LogWarning(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) Error(args ...any) {
|
||||
errors.LogError(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) Fatal(args ...any) {
|
||||
}
|
||||
|
||||
func (l *XrayLogger) Panic(args ...any) {
|
||||
}
|
||||
|
||||
func (l *XrayLogger) TraceContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
func (l *XrayLogger) DebugContext(ctx context.Context, args ...any) {
|
||||
errors.LogDebug(ctx, args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) InfoContext(ctx context.Context, args ...any) {
|
||||
errors.LogInfo(ctx, args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) WarnContext(ctx context.Context, args ...any) {
|
||||
errors.LogWarning(ctx, args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) ErrorContext(ctx context.Context, args ...any) {
|
||||
errors.LogError(ctx, args...)
|
||||
}
|
||||
|
||||
func (l *XrayLogger) FatalContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
func (l *XrayLogger) PanicContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
)
|
||||
|
||||
func CopyPacketConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, destination net.Destination, serverConn net.PacketConn) error {
|
||||
cancel := func() {
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(serverConn)
|
||||
}
|
||||
conn := &PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Dest: destination,
|
||||
Conn: inboundConn,
|
||||
T: signal.CancelAfterInactivity(ctx, cancel, 300*time.Second),
|
||||
}
|
||||
return ReturnError(bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn)))
|
||||
}
|
||||
|
||||
type PacketConnWrapper struct {
|
||||
buf.Reader
|
||||
buf.Writer
|
||||
net.Conn
|
||||
Dest net.Destination
|
||||
cached buf.MultiBuffer
|
||||
|
||||
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
|
||||
T *signal.ActivityTimer
|
||||
}
|
||||
|
||||
func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) {
|
||||
w.T.Update()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// uplinkonly
|
||||
w.T.SetTimeout(2 * time.Second)
|
||||
}
|
||||
}()
|
||||
if w.cached != nil {
|
||||
mb, bb := buf.SplitFirst(w.cached)
|
||||
if bb == nil {
|
||||
w.cached = nil
|
||||
} else {
|
||||
buffer.Write(bb.Bytes())
|
||||
w.cached = mb
|
||||
var destination net.Destination
|
||||
if bb.UDP != nil {
|
||||
destination = *bb.UDP
|
||||
} else {
|
||||
destination = w.Dest
|
||||
}
|
||||
bb.Release()
|
||||
return ToSocksaddr(destination), nil
|
||||
}
|
||||
}
|
||||
mb, err := w.ReadMultiBuffer()
|
||||
nb, bb := buf.SplitFirst(mb)
|
||||
if bb == nil {
|
||||
return M.Socksaddr{}, nil
|
||||
} else {
|
||||
buffer.Write(bb.Bytes())
|
||||
w.cached = nb
|
||||
var destination net.Destination
|
||||
if bb.UDP != nil {
|
||||
destination = *bb.UDP
|
||||
} else {
|
||||
destination = w.Dest
|
||||
}
|
||||
bb.Release()
|
||||
return ToSocksaddr(destination), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) (err error) {
|
||||
w.T.Update()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// downlinkonly
|
||||
w.T.SetTimeout(5 * time.Second)
|
||||
}
|
||||
}()
|
||||
endpoint, err := ToDestination(destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vBuf := buf.New()
|
||||
vBuf.Write(buffer.Bytes())
|
||||
vBuf.UDP = &endpoint
|
||||
return w.WriteMultiBuffer(buf.MultiBuffer{vBuf})
|
||||
}
|
||||
|
||||
func (w *PacketConnWrapper) Close() error {
|
||||
buf.ReleaseMulti(w.cached)
|
||||
return nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
)
|
||||
|
||||
func CopyConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, serverConn net.Conn) error {
|
||||
conn := &PipeConnWrapper{
|
||||
W: link.Writer,
|
||||
Conn: inboundConn,
|
||||
}
|
||||
if ir, ok := link.Reader.(io.Reader); ok {
|
||||
conn.R = ir
|
||||
} else {
|
||||
conn.R = &buf.BufferedReader{Reader: link.Reader}
|
||||
}
|
||||
cancel := func() {
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(serverConn)
|
||||
}
|
||||
conn.T = signal.CancelAfterInactivity(ctx, cancel, 300*time.Second)
|
||||
return ReturnError(bufio.CopyConn(ctx, conn, serverConn))
|
||||
}
|
||||
|
||||
type PipeConnWrapper struct {
|
||||
R io.Reader
|
||||
W buf.Writer
|
||||
net.Conn
|
||||
|
||||
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
|
||||
T *signal.ActivityTimer
|
||||
}
|
||||
|
||||
func (w *PipeConnWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *PipeConnWrapper) Read(b []byte) (n int, err error) {
|
||||
w.T.Update()
|
||||
n, err = w.R.Read(b)
|
||||
if err != nil {
|
||||
// uplinkonly
|
||||
w.T.SetTimeout(2 * time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *PipeConnWrapper) Write(p []byte) (n int, err error) {
|
||||
w.T.Update()
|
||||
n = len(p)
|
||||
var mb buf.MultiBuffer
|
||||
pLen := len(p)
|
||||
for pLen > 0 {
|
||||
buffer := buf.New()
|
||||
if pLen > buf.Size {
|
||||
_, err = buffer.Write(p[:buf.Size])
|
||||
p = p[buf.Size:]
|
||||
} else {
|
||||
buffer.Write(p)
|
||||
}
|
||||
pLen -= int(buffer.Len())
|
||||
mb = append(mb, buffer)
|
||||
}
|
||||
err = w.W.WriteMultiBuffer(mb)
|
||||
if err != nil {
|
||||
n = 0
|
||||
buf.ReleaseMulti(mb)
|
||||
// downlinkonly
|
||||
w.T.SetTimeout(5 * time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package singbridge
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
var (
|
||||
_ buf.Reader = (*Conn)(nil)
|
||||
_ buf.TimeoutReader = (*Conn)(nil)
|
||||
_ buf.Writer = (*Conn)(nil)
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
writer N.VectorisedWriter
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn) *Conn {
|
||||
writer, _ := bufio.CreateVectorisedWriter(conn)
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
buffer, err := buf.ReadBuffer(c.Conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.MultiBuffer{buffer}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) ReadMultiBufferTimeout(duration time.Duration) (buf.MultiBuffer, error) {
|
||||
err := c.SetReadDeadline(time.Now().Add(duration))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer c.SetReadDeadline(time.Time{})
|
||||
return c.ReadMultiBuffer()
|
||||
}
|
||||
|
||||
func (c *Conn) WriteMultiBuffer(bufferList buf.MultiBuffer) error {
|
||||
defer buf.ReleaseMulti(bufferList)
|
||||
if c.writer != nil {
|
||||
bytesList := make([][]byte, len(bufferList))
|
||||
for i, buffer := range bufferList {
|
||||
bytesList[i] = buffer.Bytes()
|
||||
}
|
||||
return common.Error(bufio.WriteVectorised(c.writer, bytesList))
|
||||
}
|
||||
// Since this conn is only used by tun, we don't force buffer writes to merge.
|
||||
for _, buffer := range bufferList {
|
||||
_, err := c.Conn.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -18,21 +18,19 @@ require (
|
||||
github.com/pires/go-proxyproto v0.15.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.5.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v1.1.1
|
||||
google.golang.org/grpc v1.84.0
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/grpc v1.83.2
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
@@ -57,9 +55,9 @@ require (
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,10 +2,16 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
@@ -70,10 +76,6 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
@@ -85,6 +87,18 @@ github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguH
|
||||
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
|
||||
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
@@ -93,8 +107,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
@@ -103,12 +117,12 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -116,14 +130,14 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -139,14 +153,14 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
golang.zx2c4.com/wireguard/windows v1.1.1 h1:8/H97U1v1PNDNcBsMZgU3KFuND9MQdTsU2NOwmCXArE=
|
||||
golang.zx2c4.com/wireguard/windows v1.1.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
|
||||
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type MasqueClientConfig struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
RemoteDNS []string `json:"remoteDNS"`
|
||||
}
|
||||
|
||||
func (c *MasqueClientConfig) Build() (proto.Message, error) {
|
||||
if c.Address == nil {
|
||||
return nil, errors.New(`MASQUE: "address" is not set`)
|
||||
}
|
||||
if c.Port == 0 {
|
||||
return nil, errors.New(`MASQUE: "port" is not set`)
|
||||
}
|
||||
for _, s := range c.RemoteDNS {
|
||||
if _, err := netip.ParseAddr(s); err != nil {
|
||||
return nil, errors.New(`MASQUE: invalid "remoteDNS" `, s).Base(err)
|
||||
}
|
||||
}
|
||||
return &masque.ClientConfig{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: c.Address.Build(),
|
||||
Port: uint32(c.Port),
|
||||
},
|
||||
RemoteDns: c.RemoteDNS,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/transport/internet/masque"
|
||||
)
|
||||
|
||||
func TestMasqueConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(MasqueConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{Path: "/.well-known/masque/ip/*/*/"},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"host": "example.com:8443",
|
||||
"path": "/.well-known/masque/ip/{target}/{ipproto}/",
|
||||
"headers": {"Authorization": "Basic dTpw"}
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{
|
||||
Host: "example.com:8443",
|
||||
Path: "/.well-known/masque/ip/*/*/",
|
||||
Headers: map[string]string{"Authorization": "Basic dTpw"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{"path": "/masque/ip{?target,ipproto}"}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*"},
|
||||
},
|
||||
})
|
||||
|
||||
for _, input := range []string{
|
||||
`{"path": "/masque/{target}/{ipproto}/{dns}"}`,
|
||||
`{"path": "masque"}`,
|
||||
`{"host": "example.com/path"}`,
|
||||
`{"headers": {"host": "example.com"}}`,
|
||||
`{"headers": {"Capsule-Protocol": "?0"}}`,
|
||||
`{"headers": {"X Token": "a"}}`,
|
||||
`{"headers": {"X-Token": "a\r\nb"}}`,
|
||||
} {
|
||||
if _, err := loadJSON(creator)(input); err == nil {
|
||||
t.Errorf("expected an error for %s", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasqueOutboundConfig(t *testing.T) {
|
||||
build := func(s string) error {
|
||||
c := new(OutboundDetourConfig)
|
||||
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := build(`{
|
||||
"protocol": "masque",
|
||||
"settings": {"address": "example.com", "port": 443},
|
||||
"streamSettings": {"network": "masque", "security": "tls"},
|
||||
"mux": {"enabled": false, "concurrency": -1}
|
||||
}`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
for _, input := range []string{
|
||||
`{"protocol": "masque", "settings": {"address": "example.com"}, "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true, "concurrency": -1}}`,
|
||||
`{"protocol": "freedom", "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||
} {
|
||||
if err := build(input); err == nil {
|
||||
t.Errorf("expected an error for %s", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package conf
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
@@ -55,7 +53,7 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
||||
v.Users = v.Clients
|
||||
}
|
||||
|
||||
if C.Contains(shadowaead_2022.List, v.Cipher) {
|
||||
if shadowsocks_2022.IsSupportedMethod(v.Cipher) {
|
||||
return buildShadowsocks2022(v)
|
||||
}
|
||||
|
||||
@@ -216,7 +214,7 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) {
|
||||
|
||||
if len(v.Servers) == 1 {
|
||||
server := v.Servers[0]
|
||||
if C.Contains(shadowaead_2022.List, server.Cipher) {
|
||||
if shadowsocks_2022.IsSupportedMethod(server.Cipher) {
|
||||
if server.Address == nil {
|
||||
return nil, errors.New("Shadowsocks server address is not set.")
|
||||
}
|
||||
@@ -238,7 +236,7 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) {
|
||||
|
||||
config := new(shadowsocks.ClientConfig)
|
||||
for _, server := range v.Servers {
|
||||
if C.Contains(shadowaead_2022.List, server.Cipher) {
|
||||
if shadowsocks_2022.IsSupportedMethod(server.Cipher) {
|
||||
return nil, errors.New("Shadowsocks 2022 accept no multi servers")
|
||||
}
|
||||
if server.Address == nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
||||
@@ -908,13 +909,22 @@ func (c *Realm) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
type UDPHop struct {
|
||||
Mode string `json:"mode"`
|
||||
Interval Int32Range `json:"interval"`
|
||||
RemoteIPs []string `json:"remoteIPs"`
|
||||
RemotePorts PortList `json:"remotePorts"`
|
||||
Sockopt *SocketConfig `json:"sockopt"`
|
||||
Mode string `json:"mode"`
|
||||
Interval Int32Range `json:"interval"`
|
||||
RemotePorts PortList `json:"remotePorts"`
|
||||
RemoteIPs []string `json:"remoteIPs"`
|
||||
}
|
||||
|
||||
func (c *UDPHop) Build() (proto.Message, error) {
|
||||
var sockopt *internet.SocketConfig
|
||||
if c.Sockopt != nil {
|
||||
var err error
|
||||
sockopt, err = c.Sockopt.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var local, remote, remoteOnce bool
|
||||
for _, mode := range strings.Split(c.Mode, ",") {
|
||||
switch strings.ToLower(mode) {
|
||||
@@ -943,13 +953,14 @@ func (c *UDPHop) Build() (proto.Message, error) {
|
||||
return nil, errors.New("invalid ip ", ip)
|
||||
}
|
||||
return &udphop.Config{
|
||||
Sockopt: sockopt,
|
||||
Local: local,
|
||||
Remote: remote,
|
||||
RemoteOnce: remoteOnce,
|
||||
IntervalMin: int64(c.Interval.From),
|
||||
IntervalMax: int64(c.Interval.To),
|
||||
RemoteIPs: remoteIPs,
|
||||
RemotePorts: c.RemotePorts.Build().Ports(),
|
||||
RemoteIPs: remoteIPs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,6 @@ func (p TransportProtocol) Build() (string, error) {
|
||||
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
|
||||
case "hysteria":
|
||||
return "hysteria", nil
|
||||
case "masque":
|
||||
return "masque", nil
|
||||
case "xdrive":
|
||||
return "xdrive", nil
|
||||
default:
|
||||
return "", errors.New("Config: unknown transport protocol: ", p)
|
||||
}
|
||||
@@ -63,8 +59,6 @@ type StreamConfig struct {
|
||||
WSSettings *WebSocketConfig `json:"wsSettings"`
|
||||
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
|
||||
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
|
||||
MASQUESettings *MasqueConfig `json:"masqueSettings"`
|
||||
XDRIVESettings *XDriveConfig `json:"xdriveSettings"`
|
||||
SocketSettings *SocketConfig `json:"sockopt"`
|
||||
}
|
||||
|
||||
@@ -198,26 +192,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
Settings: serial.ToTypedMessage(hs),
|
||||
})
|
||||
}
|
||||
if c.MASQUESettings != nil {
|
||||
ms, err := c.MASQUESettings.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to build MASQUE config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "masque",
|
||||
Settings: serial.ToTypedMessage(ms),
|
||||
})
|
||||
}
|
||||
if c.XDRIVESettings != nil {
|
||||
xs, err := c.XDRIVESettings.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to build XDRIVE config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "xdrive",
|
||||
Settings: serial.ToTypedMessage(xs),
|
||||
})
|
||||
}
|
||||
if c.SocketSettings != nil {
|
||||
ss, err := c.SocketSettings.Build()
|
||||
if err != nil {
|
||||
|
||||
@@ -20,12 +20,9 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/httpupgrade"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria"
|
||||
"github.com/xtls/xray-core/transport/internet/kcp"
|
||||
"github.com/xtls/xray-core/transport/internet/masque"
|
||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
"github.com/xtls/xray-core/transport/internet/tcp"
|
||||
"github.com/xtls/xray-core/transport/internet/websocket"
|
||||
"github.com/xtls/xray-core/transport/internet/xdrive"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -788,46 +785,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type MasqueConfig struct {
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
path := c.Path
|
||||
if path == "" {
|
||||
path = masque.DefaultPath
|
||||
}
|
||||
path = strings.NewReplacer(
|
||||
"{target}", "*", "{ipproto}", "*",
|
||||
"{?target,ipproto}", "?target=*&ipproto=*", "{?ipproto,target}", "?ipproto=*&target=*",
|
||||
"{&target,ipproto}", "&target=*&ipproto=*", "{&ipproto,target}", "&ipproto=*&target=*",
|
||||
).Replace(path)
|
||||
if !strings.HasPrefix(path, "/") || strings.ContainsAny(path, "{}") {
|
||||
return nil, errors.New(`invalid "path": `, path, `, only the variables {target} and {ipproto} are supported`)
|
||||
}
|
||||
if c.Host != "" {
|
||||
if u, err := url.Parse("https://" + c.Host); err != nil || u.Host != c.Host {
|
||||
return nil, errors.New(`invalid "host": `, c.Host)
|
||||
}
|
||||
}
|
||||
for k, v := range c.Headers {
|
||||
if !httpguts.ValidHeaderFieldName(k) || !httpguts.ValidHeaderFieldValue(v) {
|
||||
return nil, errors.New(`invalid header in "headers": `, strconv.Quote(k))
|
||||
}
|
||||
switch strings.ToLower(k) {
|
||||
case "host", "capsule-protocol":
|
||||
return nil, errors.New(`"headers" can't contain "`, k, `"`)
|
||||
}
|
||||
}
|
||||
return &masque.Config{
|
||||
Host: c.Host,
|
||||
Path: path,
|
||||
Headers: c.Headers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readFileOrString(f string, s []string) ([]byte, error) {
|
||||
if len(f) > 0 {
|
||||
return filesystem.ReadCert(f)
|
||||
@@ -837,50 +794,3 @@ func readFileOrString(f string, s []string) ([]byte, error) {
|
||||
}
|
||||
return nil, errors.New("both file and bytes are empty.")
|
||||
}
|
||||
|
||||
type XDriveConfig struct {
|
||||
RemoteFolder string `json:"remoteFolder"`
|
||||
Service string `json:"service"`
|
||||
Secrets []string `json:"secrets"`
|
||||
SegmentBytes uint32 `json:"segmentBytes"`
|
||||
FlushIntervalMs uint32 `json:"flushIntervalMs"`
|
||||
PollIntervalMs uint32 `json:"pollIntervalMs"`
|
||||
MaxPollIntervalMs uint32 `json:"maxPollIntervalMs"`
|
||||
SessionTTLSeconds uint32 `json:"sessionTtlSeconds"`
|
||||
Concurrency uint32 `json:"concurrency"`
|
||||
EagerWindowMs uint32 `json:"eagerWindowMs"`
|
||||
HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
|
||||
Template json.RawMessage `json:"template"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||
switch c.Service {
|
||||
case "local":
|
||||
case "Google Drive":
|
||||
if len(c.Secrets) != 3 {
|
||||
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
|
||||
}
|
||||
case "template":
|
||||
if len(c.Template) == 0 {
|
||||
return nil, errors.New(`service "template" needs a "template" object`)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported service")
|
||||
}
|
||||
config := &xdrive.Config{
|
||||
RemoteFolder: c.RemoteFolder,
|
||||
Service: c.Service,
|
||||
Secrets: c.Secrets,
|
||||
SegmentBytes: c.SegmentBytes,
|
||||
FlushIntervalMs: c.FlushIntervalMs,
|
||||
PollIntervalMs: c.PollIntervalMs,
|
||||
MaxPollIntervalMs: c.MaxPollIntervalMs,
|
||||
SessionTtlSeconds: c.SessionTTLSeconds,
|
||||
Concurrency: c.Concurrency,
|
||||
EagerWindowMs: c.EagerWindowMs,
|
||||
HoleTimeoutMs: c.HoleTimeoutMs,
|
||||
Template: string(c.Template),
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -291,76 +291,3 @@ func TestHeaderCustomUDPBuildRejectsExprWithoutArgs(t *testing.T) {
|
||||
t.Fatalf("expected transform arg rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveStreamConfig(t *testing.T) {
|
||||
config := new(StreamConfig)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"method": "xdrive",
|
||||
"xdriveSettings": {
|
||||
"remoteFolder": "/tmp/xdrive",
|
||||
"service": "local"
|
||||
}
|
||||
}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
|
||||
built, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
if built.ProtocolName != "xdrive" {
|
||||
t.Fatalf("ProtocolName is %q, want %q", built.ProtocolName, "xdrive")
|
||||
}
|
||||
if len(built.TransportSettings) != 1 || built.TransportSettings[0].ProtocolName != "xdrive" {
|
||||
t.Fatalf("TransportSettings is %v, want a single xdrive entry", built.TransportSettings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveRejectsUnknownService(t *testing.T) {
|
||||
config := new(XDriveConfig)
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Fatal("Build accepted an unsupported service")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveTemplateStreamConfig(t *testing.T) {
|
||||
config := new(StreamConfig)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"method": "xdrive",
|
||||
"xdriveSettings": {
|
||||
"remoteFolder": "folder",
|
||||
"service": "template",
|
||||
"secrets": ["user", "pass"],
|
||||
"template": {
|
||||
"flatten": true,
|
||||
"auth": {"type": "basic", "username": "{secret0}", "password": "{secret1}"},
|
||||
"put": {"method": "PUT", "url": "https://dav.example/{folder}/{name}"},
|
||||
"get": {"method": "GET", "url": "https://dav.example/{folder}/{name}"},
|
||||
"delete": {"method": "DELETE", "url": "https://dav.example/{folder}/{name}"},
|
||||
"list": {"method": "PROPFIND", "url": "https://dav.example/{folder}/", "namesRegex": "<d:href>/folder/([^<]+)</d:href>"}
|
||||
}
|
||||
}
|
||||
}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
built, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
if built.ProtocolName != "xdrive" {
|
||||
t.Fatalf("ProtocolName is %q, want xdrive", built.ProtocolName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveTemplateNeedsTemplate(t *testing.T) {
|
||||
config := new(XDriveConfig)
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Fatal("Build accepted a template service without a template")
|
||||
}
|
||||
}
|
||||
|
||||
+23
-7
@@ -59,13 +59,14 @@ func (c *WireGuardPeerConfig) Build() (*wireguard.PeerConfig, error) {
|
||||
type WireGuardConfig struct {
|
||||
IsClient bool `json:""`
|
||||
|
||||
NoKernelTun bool `json:"noKernelTun"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Address []string `json:"address"`
|
||||
Peers []*WireGuardPeerConfig `json:"peers"`
|
||||
MTU int32 `json:"mtu"`
|
||||
Reserved []byte `json:"reserved"`
|
||||
DNS []string `json:"remoteDNS"`
|
||||
NoKernelTun bool `json:"noKernelTun"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Address []string `json:"address"`
|
||||
Peers []*WireGuardPeerConfig `json:"peers"`
|
||||
MTU int32 `json:"mtu"`
|
||||
Reserved []byte `json:"reserved"`
|
||||
DomainStrategy string `json:"domainStrategy"`
|
||||
DNS []string `json:"remoteDNS"`
|
||||
}
|
||||
|
||||
func (c *WireGuardConfig) Build() (proto.Message, error) {
|
||||
@@ -124,6 +125,21 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
config.Reserved = c.Reserved
|
||||
|
||||
switch strings.ToLower(c.DomainStrategy) {
|
||||
case "forceip", "":
|
||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP
|
||||
case "forceipv4":
|
||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP4
|
||||
case "forceipv6":
|
||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP6
|
||||
case "forceipv4v6":
|
||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP46
|
||||
case "forceipv6v4":
|
||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP64
|
||||
default:
|
||||
return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy)
|
||||
}
|
||||
|
||||
config.IsClient = c.IsClient
|
||||
config.NoKernelTun = c.NoKernelTun
|
||||
config.DNS = c.DNS
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
core "github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
@@ -49,7 +48,6 @@ var (
|
||||
"vmess": func() interface{} { return new(VMessOutboundConfig) },
|
||||
"trojan": func() interface{} { return new(TrojanClientConfig) },
|
||||
"hysteria": func() interface{} { return new(HysteriaClientConfig) },
|
||||
"masque": func() interface{} { return new(MasqueClientConfig) },
|
||||
"dns": func() interface{} { return new(DNSOutboundConfig) },
|
||||
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} },
|
||||
}, "protocol", "settings")
|
||||
@@ -340,14 +338,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := ts.(*masque.ClientConfig); ok {
|
||||
if ms := senderSettings.MultiplexSettings; ms != nil && ms.Enabled {
|
||||
return nil, errors.New(`masque outbound does not support "mux"`)
|
||||
}
|
||||
} else if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.ProtocolName == "masque" {
|
||||
return nil, errors.New("the masque transport can only be used by the masque outbound")
|
||||
}
|
||||
|
||||
if fc, ok := ts.(*freedom.Config); ok {
|
||||
if senderSettings.StreamSettings != nil &&
|
||||
senderSettings.StreamSettings.SocketSettings != nil &&
|
||||
|
||||
@@ -41,7 +41,6 @@ import (
|
||||
_ "github.com/xtls/xray-core/proxy/freedom"
|
||||
_ "github.com/xtls/xray-core/proxy/http"
|
||||
_ "github.com/xtls/xray-core/proxy/loopback"
|
||||
_ "github.com/xtls/xray-core/proxy/masque"
|
||||
_ "github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
_ "github.com/xtls/xray-core/proxy/socks"
|
||||
_ "github.com/xtls/xray-core/proxy/trojan"
|
||||
@@ -55,14 +54,12 @@ import (
|
||||
_ "github.com/xtls/xray-core/transport/internet/grpc"
|
||||
_ "github.com/xtls/xray-core/transport/internet/httpupgrade"
|
||||
_ "github.com/xtls/xray-core/transport/internet/kcp"
|
||||
_ "github.com/xtls/xray-core/transport/internet/masque"
|
||||
_ "github.com/xtls/xray-core/transport/internet/reality"
|
||||
_ "github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
_ "github.com/xtls/xray-core/transport/internet/tcp"
|
||||
_ "github.com/xtls/xray-core/transport/internet/tls"
|
||||
_ "github.com/xtls/xray-core/transport/internet/udp"
|
||||
_ "github.com/xtls/xray-core/transport/internet/websocket"
|
||||
_ "github.com/xtls/xray-core/transport/internet/xdrive"
|
||||
|
||||
// Transport headers
|
||||
_ "github.com/xtls/xray-core/transport/internet/headers/http"
|
||||
|
||||
@@ -236,14 +236,14 @@ type UDPReader struct {
|
||||
|
||||
func (r *UDPReader) ReadFrom(p []byte) (n int, addr *net.Destination, err error) {
|
||||
for {
|
||||
var packet [1500]byte
|
||||
var buf [hysteria.MaxDatagramFrameSize]byte
|
||||
|
||||
n, err := r.reader.Read(packet[:])
|
||||
n, err := r.reader.Read(buf[:])
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
msg, err := ParseUDPMessage(packet[:n])
|
||||
msg, err := ParseUDPMessage(buf[:n])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,328 +0,0 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"io"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/proxy/wireguard"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/masque"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
establishTimeout = 10 * time.Second
|
||||
retryInterval = time.Second
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
server *protocol.ServerSpec
|
||||
policyManager policy.Manager
|
||||
remoteDNS []netip.Addr
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
tunnel atomic.Pointer[tunnel]
|
||||
|
||||
mu sync.Mutex
|
||||
lastErr error
|
||||
lastErrAt time.Time
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
|
||||
v := core.MustFromContext(ctx)
|
||||
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
|
||||
|
||||
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
|
||||
if _, ok := streamSettings.ProtocolSettings.(*masque.Config); !ok {
|
||||
return nil, errors.New("not masque transport")
|
||||
}
|
||||
if tls.ConfigFromStreamSettings(streamSettings) == nil {
|
||||
return nil, errors.New(`MASQUE requires "security": "tls"`)
|
||||
}
|
||||
if config.Server == nil {
|
||||
return nil, errors.New(`no target server found`)
|
||||
}
|
||||
server, err := protocol.NewServerSpecFromPB(config.Server)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get server spec").Base(err)
|
||||
}
|
||||
|
||||
dns := config.RemoteDns
|
||||
if len(dns) == 0 {
|
||||
dns = []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"}
|
||||
}
|
||||
remoteDNS := make([]netip.Addr, 0, len(dns))
|
||||
for _, s := range dns {
|
||||
addr, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid remote DNS server ", s).Base(err)
|
||||
}
|
||||
remoteDNS = append(remoteDNS, addr)
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
server: server,
|
||||
policyManager: p,
|
||||
remoteDNS: remoteDNS,
|
||||
}
|
||||
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
if !ob.Target.IsValid() {
|
||||
return errors.New("target not specified")
|
||||
}
|
||||
ob.Name = "masque"
|
||||
ob.CanSpliceCopy = 3
|
||||
|
||||
t, err := c.getTunnel(ctx, dialer)
|
||||
if err != nil {
|
||||
return errors.New("failed to establish CONNECT-IP tunnel").Base(err)
|
||||
}
|
||||
|
||||
var newCtx context.Context
|
||||
var newCancel context.CancelFunc
|
||||
if session.TimeoutOnlyFromContext(ctx) {
|
||||
newCtx, newCancel = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
sessionPolicy := c.policyManager.ForLevel(0)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, func() {
|
||||
cancel()
|
||||
if newCancel != nil {
|
||||
newCancel()
|
||||
}
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
if newCtx != nil {
|
||||
ctx = newCtx
|
||||
}
|
||||
|
||||
var reader buf.Reader
|
||||
var writer buf.Writer
|
||||
|
||||
switch ob.Target.Network {
|
||||
case net.Network_TCP:
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if sessionPolicy.Timeouts.Handshake != 0 {
|
||||
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, sessionPolicy.Timeouts.Handshake)
|
||||
conn, err = t.tnet.DialContext(timeoutCtx, "tcp", ob.Target.NetAddr())
|
||||
timeoutCancel()
|
||||
} else {
|
||||
conn, err = t.tnet.Dial("tcp", ob.Target.NetAddr())
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("failed to create TCP connection").Base(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
reader = buf.NewReader(conn)
|
||||
writer = buf.NewWriter(conn)
|
||||
case net.Network_UDP:
|
||||
conn, err := t.tnet.Dial("udp", ob.Target.NetAddr())
|
||||
if err != nil {
|
||||
return errors.New("failed to create UDP connection").Base(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
uc := &wireguard.UDPConnClient{
|
||||
PacketConn: conn.(*internet.PacketConnWrapper).PacketConn,
|
||||
Dest: conn.RemoteAddr().(*net.UDPAddr),
|
||||
}
|
||||
reader = uc
|
||||
writer = uc
|
||||
default:
|
||||
panic(ob.Target.Network)
|
||||
}
|
||||
|
||||
requestFunc := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseFunc := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDonePost := task.OnSuccess(responseFunc, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestFunc, responseDonePost); err != nil {
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getTunnel(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.ctx.Err() != nil {
|
||||
return nil, errors.New("closed")
|
||||
}
|
||||
if t := c.tunnel.Load(); t != nil {
|
||||
select {
|
||||
case <-t.done:
|
||||
default:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.lastErr != nil && time.Since(c.lastErrAt) < retryInterval {
|
||||
return nil, c.lastErr
|
||||
}
|
||||
|
||||
t, err := c.establish(ctx, dialer)
|
||||
if err != nil {
|
||||
c.lastErr, c.lastErrAt = err, time.Now()
|
||||
return nil, err
|
||||
}
|
||||
c.lastErr = nil
|
||||
c.tunnel.Store(t)
|
||||
if c.ctx.Err() != nil {
|
||||
if c.tunnel.CompareAndSwap(t, nil) {
|
||||
t.close()
|
||||
}
|
||||
return nil, errors.New("closed")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
|
||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), establishTimeout)
|
||||
defer cancel()
|
||||
defer context.AfterFunc(c.ctx, cancel)()
|
||||
conn, err := dialer.Dial(ctx, c.server.Destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mconn, ok := stat.TryUnwrapStatsConn(conn).(*masque.Conn)
|
||||
if !ok {
|
||||
conn.Close()
|
||||
return nil, errors.New("not a CONNECT-IP connection")
|
||||
}
|
||||
t, err := newTunnel(conn, mconn.LocalAddrs(), c.remoteDNS)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
errors.LogInfo(ctx, "MASQUE: tunnel established from ", mconn.LocalAddrs())
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.cancel()
|
||||
if t := c.tunnel.Swap(nil); t != nil {
|
||||
t.close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type tunnel struct {
|
||||
conn stat.Connection
|
||||
dev tun.Device
|
||||
tnet *wireguard.Net
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr) (*tunnel, error) {
|
||||
var dns []netip.Addr
|
||||
for _, addr := range remoteDNS {
|
||||
if slices.ContainsFunc(local, func(l netip.Addr) bool { return l.Is4() == addr.Is4() }) {
|
||||
dns = append(dns, addr)
|
||||
}
|
||||
}
|
||||
if len(dns) == 0 {
|
||||
errors.LogWarning(context.Background(), "MASQUE: no remote DNS server is reachable from the assigned addresses ", local, ", domain names will fail to resolve")
|
||||
dns = remoteDNS
|
||||
}
|
||||
|
||||
dev, tnet, _, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &tunnel{
|
||||
conn: conn,
|
||||
dev: dev,
|
||||
tnet: tnet,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go t.readFromTunnel()
|
||||
go t.writeToTunnel()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *tunnel) readFromTunnel() {
|
||||
defer t.close()
|
||||
b := make([]byte, buf.Size)
|
||||
for {
|
||||
n, err := t.conn.Read(b)
|
||||
if err != nil {
|
||||
if go_errors.Is(err, io.ErrShortBuffer) {
|
||||
continue
|
||||
}
|
||||
errors.LogInfoInner(context.Background(), err, "MASQUE: tunnel closed")
|
||||
return
|
||||
}
|
||||
t.dev.Write([][]byte{b[:n]}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tunnel) writeToTunnel() {
|
||||
bufs := [][]byte{make([]byte, masque.MinPacketSize)}
|
||||
sizes := []int{0}
|
||||
for {
|
||||
if _, err := t.dev.Read(bufs, sizes, 0); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := t.conn.Write(bufs[0][:sizes[0]]); err != nil {
|
||||
var ptb *masque.PacketTooBigError
|
||||
if go_errors.As(err, &ptb) {
|
||||
go t.dev.Write([][]byte{ptb.ICMP}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tunnel) close() {
|
||||
t.closeOnce.Do(func() {
|
||||
close(t.done)
|
||||
t.conn.Close()
|
||||
t.dev.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return NewClient(ctx, config.(*ClientConfig))
|
||||
}))
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: proxy/masque/config.proto
|
||||
|
||||
package masque
|
||||
|
||||
import (
|
||||
protocol "github.com/xtls/xray-core/common/protocol"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
||||
RemoteDns []string `protobuf:"bytes,2,rep,name=remote_dns,json=remoteDns,proto3" json:"remote_dns,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ClientConfig) Reset() {
|
||||
*x = ClientConfig{}
|
||||
mi := &file_proxy_masque_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ClientConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ClientConfig) ProtoMessage() {}
|
||||
|
||||
func (x *ClientConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_masque_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead.
|
||||
func (*ClientConfig) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_masque_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ClientConfig) GetServer() *protocol.ServerEndpoint {
|
||||
if x != nil {
|
||||
return x.Server
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientConfig) GetRemoteDns() []string {
|
||||
if x != nil {
|
||||
return x.RemoteDns
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proxy_masque_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_masque_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19proxy/masque/config.proto\x12\x11xray.proxy.masque\x1a!common/protocol/server_spec.proto\"k\n" +
|
||||
"\fClientConfig\x12<\n" +
|
||||
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\x12\x1d\n" +
|
||||
"\n" +
|
||||
"remote_dns\x18\x02 \x03(\tR\tremoteDnsBU\n" +
|
||||
"\x15com.xray.proxy.masqueP\x01Z&github.com/xtls/xray-core/proxy/masque\xaa\x02\x11Xray.Proxy.Masqueb\x06proto3"
|
||||
|
||||
var (
|
||||
file_proxy_masque_config_proto_rawDescOnce sync.Once
|
||||
file_proxy_masque_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_proxy_masque_config_proto_rawDescGZIP() []byte {
|
||||
file_proxy_masque_config_proto_rawDescOnce.Do(func() {
|
||||
file_proxy_masque_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_masque_config_proto_rawDesc), len(file_proxy_masque_config_proto_rawDesc)))
|
||||
})
|
||||
return file_proxy_masque_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proxy_masque_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_proxy_masque_config_proto_goTypes = []any{
|
||||
(*ClientConfig)(nil), // 0: xray.proxy.masque.ClientConfig
|
||||
(*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint
|
||||
}
|
||||
var file_proxy_masque_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.proxy.masque.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proxy_masque_config_proto_init() }
|
||||
func file_proxy_masque_config_proto_init() {
|
||||
if File_proxy_masque_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_masque_config_proto_rawDesc), len(file_proxy_masque_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_proxy_masque_config_proto_goTypes,
|
||||
DependencyIndexes: file_proxy_masque_config_proto_depIdxs,
|
||||
MessageInfos: file_proxy_masque_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proxy_masque_config_proto = out.File
|
||||
file_proxy_masque_config_proto_goTypes = nil
|
||||
file_proxy_masque_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.proxy.masque;
|
||||
option csharp_namespace = "Xray.Proxy.Masque";
|
||||
option go_package = "github.com/xtls/xray-core/proxy/masque";
|
||||
option java_package = "com.xray.proxy.masque";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/protocol/server_spec.proto";
|
||||
|
||||
message ClientConfig {
|
||||
xray.common.protocol.ServerEndpoint server = 1;
|
||||
repeated string remote_dns = 2;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
type CipherMethod struct {
|
||||
Name string
|
||||
KeySaltLength int
|
||||
IsChaCha bool
|
||||
}
|
||||
|
||||
var (
|
||||
cipherAES128GCM = &CipherMethod{Name: MethodAES128GCM, KeySaltLength: 16, IsChaCha: false}
|
||||
cipherAES256GCM = &CipherMethod{Name: MethodAES256GCM, KeySaltLength: 32, IsChaCha: false}
|
||||
cipherChaCha20Poly1305 = &CipherMethod{Name: MethodChaCha20Poly1305, KeySaltLength: 32, IsChaCha: true}
|
||||
)
|
||||
|
||||
func GetCipherMethod(name string) (*CipherMethod, error) {
|
||||
switch name {
|
||||
case MethodAES128GCM:
|
||||
return cipherAES128GCM, nil
|
||||
case MethodAES256GCM:
|
||||
return cipherAES256GCM, nil
|
||||
case MethodChaCha20Poly1305:
|
||||
return cipherChaCha20Poly1305, nil
|
||||
default:
|
||||
return nil, errors.New("unknown shadowsocks 2022 method")
|
||||
}
|
||||
}
|
||||
|
||||
// NewAEAD creates standard stream AEAD cipher instance (AES-GCM or ChaCha20-Poly1305)
|
||||
func (m *CipherMethod) NewAEAD(key []byte) (cipher.AEAD, error) {
|
||||
if m.IsChaCha {
|
||||
return chacha20poly1305.New(key)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipher.NewGCM(block)
|
||||
}
|
||||
|
||||
// NewBlock creates standard 16-byte block cipher for AES header encryption/decryption
|
||||
func (m *CipherMethod) NewBlock(key []byte) (cipher.Block, error) {
|
||||
return aes.NewCipher(key)
|
||||
}
|
||||
|
||||
// NewUDPCipher creates AEAD cipher for UDP packets (XChaCha20-Poly1305 with 24-byte nonce)
|
||||
func (m *CipherMethod) NewUDPCipher(key []byte) (cipher.AEAD, error) {
|
||||
if m.IsChaCha {
|
||||
return chacha20poly1305.NewX(key)
|
||||
}
|
||||
return nil, errors.New("shadowsocks-2022: udp separate AEAD cipher only available for chacha20 method")
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
@@ -8,26 +11,31 @@ import (
|
||||
|
||||
// MemoryAccount is an account type converted from Account.
|
||||
type MemoryAccount struct {
|
||||
Key string
|
||||
Key []byte
|
||||
}
|
||||
|
||||
// AsAccount implements protocol.AsAccount.
|
||||
func (u *Account) AsAccount() (protocol.Account, error) {
|
||||
keyStr := u.GetKey()
|
||||
raw, err := base64.StdEncoding.DecodeString(keyStr)
|
||||
if err != nil {
|
||||
raw = []byte(keyStr)
|
||||
}
|
||||
return &MemoryAccount{
|
||||
Key: u.GetKey(),
|
||||
Key: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Equals implements protocol.Account.Equals().
|
||||
func (a *MemoryAccount) Equals(another protocol.Account) bool {
|
||||
if account, ok := another.(*MemoryAccount); ok {
|
||||
return a.Key == account.Key
|
||||
return bytes.Equal(a.Key, account.Key)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *MemoryAccount) ToProto() proto.Message {
|
||||
return &Account{
|
||||
Key: a.Key,
|
||||
Key: base64.StdEncoding.EncodeToString(a.Key),
|
||||
}
|
||||
}
|
||||
|
||||
+208
-126
@@ -2,17 +2,11 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
shadowsocks "github.com/sagernet/sing-shadowsocks"
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/antireplay"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
@@ -20,7 +14,10 @@ import (
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/common/singbridge"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
@@ -32,10 +29,13 @@ func init() {
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
networks []net.Network
|
||||
service shadowsocks.Service
|
||||
email string
|
||||
level int
|
||||
networks []net.Network
|
||||
method *CipherMethod
|
||||
psk []byte
|
||||
user *protocol.MemoryUser
|
||||
saltFilter *antireplay.ReplayFilter[[32]byte]
|
||||
udpCodec *UDPServerCodec
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
|
||||
@@ -46,20 +46,35 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
|
||||
net.Network_UDP,
|
||||
}
|
||||
}
|
||||
inbound := &Inbound{
|
||||
networks: networks,
|
||||
email: config.Email,
|
||||
level: int(config.Level),
|
||||
}
|
||||
if !C.Contains(shadowaead_2022.List, config.Method) {
|
||||
return nil, errors.New("unsupported method ", config.Method)
|
||||
}
|
||||
service, err := shadowaead_2022.NewServiceWithPassword(config.Method, config.Key, 500, inbound, nil)
|
||||
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
return nil, errors.New("unsupported method: ", config.Method).Base(err)
|
||||
}
|
||||
inbound.service = service
|
||||
return inbound, nil
|
||||
|
||||
psk, err := ParseKey(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
udpCodec, err := NewUDPServerCodec(method, psk, 500*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
return &Inbound{
|
||||
networks: networks,
|
||||
method: method,
|
||||
psk: psk,
|
||||
saltFilter: antireplay.NewMapFilter[[32]byte](60),
|
||||
user: &protocol.MemoryUser{
|
||||
Email: config.Email,
|
||||
Level: uint32(config.Level),
|
||||
},
|
||||
udpCodec: udpCodec,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *Inbound) Network() []net.Network {
|
||||
@@ -70,114 +85,181 @@ func (i *Inbound) Process(ctx context.Context, network net.Network, connection s
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.Name = "shadowsocks-2022"
|
||||
inbound.CanSpliceCopy = 3
|
||||
|
||||
var metadata M.Metadata
|
||||
if inbound.Source.IsValid() {
|
||||
metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr())
|
||||
}
|
||||
|
||||
ctx = session.ContextWithDispatcher(ctx, dispatcher)
|
||||
inbound.User = i.user
|
||||
|
||||
if network == net.Network_TCP {
|
||||
return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
|
||||
} else {
|
||||
reader := buf.NewReader(connection)
|
||||
pc := &natPacketConn{connection}
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
return i.processTCP(ctx, connection, dispatcher)
|
||||
}
|
||||
return i.processUDP(ctx, connection, dispatcher)
|
||||
}
|
||||
|
||||
func (i *Inbound) processTCP(ctx context.Context, conn net.Conn, dispatcher routing.Dispatcher) error {
|
||||
defer conn.Close()
|
||||
|
||||
sessionPolicy := i.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
var salt [32]byte
|
||||
saltSlice := salt[:i.method.KeySaltLength]
|
||||
if _, err := io.ReadFull(conn, saltSlice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !i.saltFilter.Check(salt) {
|
||||
return ErrSaltNotUnique
|
||||
}
|
||||
|
||||
sessionKey := DeriveSessionSubKey(i.psk, saltSlice, i.method.KeySaltLength)
|
||||
aead, err := i.method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := NewStreamReader(conn, aead)
|
||||
|
||||
reqHeader, err := ReadClientRequestHeader(conn, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
dest := reqHeader.Destination
|
||||
|
||||
writer, err := WriteTCPResponse(conn, i.method, i.psk, saltSlice, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.user.Email,
|
||||
})
|
||||
|
||||
errors.LogInfo(ctx, "tunneling request to ", dest)
|
||||
|
||||
link, err := dispatcher.Dispatch(ctx, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(reqHeader.EarlyData) > 0 {
|
||||
earlyBuf := buf.New()
|
||||
earlyBuf.Write(reqHeader.EarlyData)
|
||||
if err := link.Writer.WriteMultiBuffer(buf.MultiBuffer{earlyBuf}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sessionPolicy = i.policyManager.ForLevel(uint32(i.user.Level))
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
|
||||
}
|
||||
|
||||
func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
udpConns := utils.NewTypedSyncMap[uint64, *udpConnEntry]()
|
||||
defer func() {
|
||||
udpConns.Range(func(key uint64, entry *udpConnEntry) bool {
|
||||
entry.timer.SetTimeout(0)
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
reader := buf.NewReader(conn)
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, b := range mb {
|
||||
decoded, err := i.udpCodec.DecodePacket(b.Bytes())
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return singbridge.ReturnError(err)
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
for _, buffer := range mb {
|
||||
packet := B.As(buffer.Bytes()).ToOwned()
|
||||
buffer.Release()
|
||||
err = i.service.NewPacket(ctx, pc, packet, metadata)
|
||||
|
||||
entry, ok := udpConns.Load(decoded.SessionID)
|
||||
if !ok {
|
||||
sessCtx, cancel := context.WithCancel(ctx)
|
||||
sessCtx = log.ContextWithAccessMessage(sessCtx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: decoded.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.user.Email,
|
||||
})
|
||||
|
||||
link, err := dispatcher.Dispatch(sessCtx, decoded.Destination)
|
||||
if err != nil {
|
||||
packet.Release()
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
cancel()
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
newEntry := &udpConnEntry{
|
||||
link: link,
|
||||
cancel: cancel,
|
||||
}
|
||||
sessionPolicy := i.policyManager.ForLevel(uint32(i.user.Level))
|
||||
newEntry.timer = signal.CancelAfterInactivity(sessCtx, func() {
|
||||
udpConns.Delete(decoded.SessionID)
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
cancel()
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
actual, loaded := udpConns.LoadOrStore(decoded.SessionID, newEntry)
|
||||
if loaded {
|
||||
// Another goroutine/packet beat us to storing, terminate our redundant link
|
||||
newEntry.timer.SetTimeout(0)
|
||||
entry = actual
|
||||
} else {
|
||||
entry = newEntry
|
||||
go func(sessID uint64, dest net.Destination, cEntry *udpConnEntry) {
|
||||
defer func() {
|
||||
cEntry.timer.SetTimeout(0)
|
||||
}()
|
||||
for {
|
||||
resMb, err := cEntry.link.Reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cEntry.timer.Update()
|
||||
for _, rb := range resMb {
|
||||
encPacket, err := i.udpCodec.EncodePacket(sessID, dest, rb.Bytes())
|
||||
rb.Release()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = conn.Write(encPacket)
|
||||
}
|
||||
}
|
||||
}(decoded.SessionID, decoded.Destination, entry)
|
||||
}
|
||||
}
|
||||
|
||||
entry.timer.Update()
|
||||
payloadBuf := buf.New()
|
||||
payloadBuf.Write(decoded.Payload)
|
||||
b.Release()
|
||||
_ = entry.link.Writer.WriteMultiBuffer(buf.MultiBuffer{payloadBuf})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: i.email,
|
||||
Level: uint32(i.level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return singbridge.CopyConn(ctx, nil, link, conn)
|
||||
}
|
||||
|
||||
func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: i.email,
|
||||
Level: uint32(i.level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outConn := &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
return bufio.CopyPacketConn(ctx, conn, outConn)
|
||||
}
|
||||
|
||||
func (i *Inbound) NewError(ctx context.Context, err error) {
|
||||
if E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
errors.LogWarning(ctx, err.Error())
|
||||
}
|
||||
|
||||
type natPacketConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *natPacketConn) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) {
|
||||
_, err = buffer.ReadFrom(c)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *natPacketConn) WritePacket(buffer *B.Buffer, addr M.Socksaddr) error {
|
||||
_, err := buffer.WriteTo(c)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,21 +2,17 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
A "github.com/sagernet/sing/common/auth"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/antireplay"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
@@ -24,8 +20,11 @@ import (
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/common/singbridge"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
@@ -38,9 +37,16 @@ func init() {
|
||||
|
||||
type MultiUserInbound struct {
|
||||
sync.Mutex
|
||||
networks []net.Network
|
||||
users []*protocol.MemoryUser
|
||||
service *shadowaead_2022.MultiService[int]
|
||||
networks []net.Network
|
||||
method *CipherMethod
|
||||
masterPSK []byte
|
||||
usersByHash *utils.TypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser]
|
||||
usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser]
|
||||
userCount atomic.Int64
|
||||
saltFilter *antireplay.ReplayFilter[[32]byte]
|
||||
udpSessions *UDPSessionManager
|
||||
udpMasterCipher cipher.Block
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiUserInbound, error) {
|
||||
@@ -51,138 +57,131 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
|
||||
net.Network_UDP,
|
||||
}
|
||||
}
|
||||
memUsers := []*protocol.MemoryUser{}
|
||||
for i, user := range config.Users {
|
||||
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if method.IsChaCha {
|
||||
return nil, errors.New("shadowsocks 2022 multi-user: only aes methods are supported")
|
||||
}
|
||||
|
||||
masterPSK, err := ParseKey(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
masterBlock, err := method.NewBlock(masterPSK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
i := &MultiUserInbound{
|
||||
networks: networks,
|
||||
method: method,
|
||||
masterPSK: masterPSK,
|
||||
usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](),
|
||||
usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](),
|
||||
saltFilter: antireplay.NewMapFilter[[32]byte](60),
|
||||
udpSessions: NewUDPSessionManager(500 * time.Second),
|
||||
udpMasterCipher: masterBlock,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}
|
||||
|
||||
for idx, user := range config.Users {
|
||||
if user.Email == "" {
|
||||
u := uuid.New()
|
||||
user.Email = "unnamed-user-" + strconv.Itoa(i) + "-" + u.String()
|
||||
user.Email = "unnamed-user-" + strconv.Itoa(idx) + "-" + u.String()
|
||||
}
|
||||
u, err := user.ToMemoryUser()
|
||||
memUser, err := user.ToMemoryUser()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get shadowsocks user").Base(err).AtError()
|
||||
return nil, errors.New("failed to parse shadowsocks user").Base(err)
|
||||
}
|
||||
if err := i.AddUser(ctx, memUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
memUsers = append(memUsers, u)
|
||||
}
|
||||
|
||||
inbound := &MultiUserInbound{
|
||||
networks: networks,
|
||||
users: memUsers,
|
||||
}
|
||||
if config.Key == "" {
|
||||
return nil, errors.New("missing key")
|
||||
}
|
||||
psk, err := base64.StdEncoding.DecodeString(config.Key)
|
||||
if err != nil {
|
||||
return nil, errors.New("parse config").Base(err)
|
||||
}
|
||||
service, err := shadowaead_2022.NewMultiService[int](config.Method, psk, 500, inbound, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
}
|
||||
err = service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(memUsers, func(index int, it *protocol.MemoryUser) int { return index }),
|
||||
C.Map(memUsers, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
}
|
||||
|
||||
inbound.service = service
|
||||
return inbound, nil
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// AddUser implements proxy.UserManager.AddUser().
|
||||
// AddUser implements proxy.UserManager.AddUser()
|
||||
func (i *MultiUserInbound) AddUser(ctx context.Context, u *protocol.MemoryUser) error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
var emailKey string
|
||||
if u.Email != "" {
|
||||
for idx := range i.users {
|
||||
if i.users[idx].Email == u.Email {
|
||||
return errors.New("User ", u.Email, " already exists.")
|
||||
}
|
||||
emailKey = strings.ToLower(u.Email)
|
||||
if _, exists := i.usersByEmail.Load(emailKey); exists {
|
||||
return errors.New("user ", u.Email, " already exists")
|
||||
}
|
||||
}
|
||||
i.users = append(i.users, u)
|
||||
|
||||
// sync to multi service
|
||||
// Considering implements shadowsocks2022 in xray-core may have better performance.
|
||||
i.service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(i.users, func(index int, it *protocol.MemoryUser) int { return index }),
|
||||
C.Map(i.users, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }),
|
||||
)
|
||||
memAcc, ok := u.Account.(*MemoryAccount)
|
||||
if !ok {
|
||||
return errors.New("missing or invalid user account")
|
||||
}
|
||||
|
||||
if len(memAcc.Key) != i.method.KeySaltLength {
|
||||
return ErrBadKey
|
||||
}
|
||||
|
||||
pskHash := DeriveUserPSKHash(memAcc.Key)
|
||||
i.usersByHash.Store(pskHash, u)
|
||||
if emailKey != "" {
|
||||
i.usersByEmail.Store(emailKey, u)
|
||||
}
|
||||
i.userCount.Add(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUser implements proxy.UserManager.RemoveUser().
|
||||
// RemoveUser implements proxy.UserManager.RemoveUser()
|
||||
func (i *MultiUserInbound) RemoveUser(ctx context.Context, email string) error {
|
||||
if email == "" {
|
||||
return errors.New("Email must not be empty.")
|
||||
return errors.New("email must not be empty")
|
||||
}
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
idx := -1
|
||||
for ii, u := range i.users {
|
||||
if strings.EqualFold(u.Email, email) {
|
||||
idx = ii
|
||||
break
|
||||
}
|
||||
emailKey := strings.ToLower(email)
|
||||
u, loaded := i.usersByEmail.LoadAndDelete(emailKey)
|
||||
if !loaded {
|
||||
return errors.New("user ", email, " not found")
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
return errors.New("User ", email, " not found.")
|
||||
}
|
||||
|
||||
ulen := len(i.users)
|
||||
|
||||
i.users[idx] = i.users[ulen-1]
|
||||
i.users[ulen-1] = nil
|
||||
i.users = i.users[:ulen-1]
|
||||
|
||||
// sync to multi service
|
||||
// Considering implements shadowsocks2022 in xray-core may have better performance.
|
||||
i.service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(i.users, func(index int, it *protocol.MemoryUser) int { return index }),
|
||||
C.Map(i.users, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }),
|
||||
)
|
||||
pskHash := DeriveUserPSKHash(u.Account.(*MemoryAccount).Key)
|
||||
i.usersByHash.Delete(pskHash)
|
||||
i.userCount.Add(-1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser implements proxy.UserManager.GetUser().
|
||||
// GetUser implements proxy.UserManager.GetUser()
|
||||
func (i *MultiUserInbound) GetUser(ctx context.Context, email string) *protocol.MemoryUser {
|
||||
if email == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
for _, u := range i.users {
|
||||
if strings.EqualFold(u.Email, email) {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return nil
|
||||
u, _ := i.usersByEmail.Load(strings.ToLower(email))
|
||||
return u
|
||||
}
|
||||
|
||||
// GetUsers implements proxy.UserManager.GetUsers().
|
||||
// GetUsers implements proxy.UserManager.GetUsers()
|
||||
func (i *MultiUserInbound) GetUsers(ctx context.Context) []*protocol.MemoryUser {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
dst := make([]*protocol.MemoryUser, len(i.users))
|
||||
copy(dst, i.users)
|
||||
return dst
|
||||
var users []*protocol.MemoryUser
|
||||
i.usersByEmail.Range(func(_ string, user *protocol.MemoryUser) bool {
|
||||
users = append(users, user)
|
||||
return true
|
||||
})
|
||||
return users
|
||||
}
|
||||
|
||||
// GetUsersCount implements proxy.UserManager.GetUsersCount().
|
||||
// GetUsersCount implements proxy.UserManager.GetUsersCount()
|
||||
func (i *MultiUserInbound) GetUsersCount(context.Context) int64 {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
return int64(len(i.users))
|
||||
return i.userCount.Load()
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) Network() []net.Network {
|
||||
@@ -194,97 +193,319 @@ func (i *MultiUserInbound) Process(ctx context.Context, network net.Network, con
|
||||
inbound.Name = "shadowsocks-2022-multi"
|
||||
inbound.CanSpliceCopy = 3
|
||||
|
||||
var metadata M.Metadata
|
||||
if inbound.Source.IsValid() {
|
||||
metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr())
|
||||
if network == net.Network_TCP {
|
||||
return i.processTCP(ctx, connection, dispatcher)
|
||||
}
|
||||
return i.processUDP(ctx, connection, dispatcher)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) processTCP(ctx context.Context, conn net.Conn, dispatcher routing.Dispatcher) error {
|
||||
defer conn.Close()
|
||||
|
||||
sessionPolicy := i.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
ctx = session.ContextWithDispatcher(ctx, dispatcher)
|
||||
// 1. Read Request Salt (16 or 32 bytes)
|
||||
var salt [32]byte
|
||||
saltSlice := salt[:i.method.KeySaltLength]
|
||||
if _, err := io.ReadFull(conn, saltSlice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if network == net.Network_TCP {
|
||||
return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
|
||||
} else {
|
||||
reader := buf.NewReader(connection)
|
||||
pc := &natPacketConn{connection}
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return singbridge.ReturnError(err)
|
||||
if !i.saltFilter.Check(salt) {
|
||||
return ErrSaltNotUnique
|
||||
}
|
||||
|
||||
// 2. Read Extended Identity Header (16 bytes)
|
||||
var eih [AESBlockSize]byte
|
||||
if _, err := io.ReadFull(conn, eih[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decrypt EIH with IdentitySubKey derived from masterPSK and salt
|
||||
identitySubkey := DeriveIdentitySubKey(i.masterPSK, saltSlice, i.method.KeySaltLength)
|
||||
block, err := i.method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var decryptedHash [AESBlockSize]byte
|
||||
block.Decrypt(decryptedHash[:], eih[:])
|
||||
|
||||
// Lookup user
|
||||
user, ok := i.usersByHash.Load(decryptedHash)
|
||||
if !ok || user == nil {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
userPSK := user.Account.(*MemoryAccount).Key
|
||||
|
||||
// 3. Derive Session Subkey using matched user's PSK
|
||||
sessionKey := DeriveSessionSubKey(userPSK, saltSlice, i.method.KeySaltLength)
|
||||
aead, err := i.method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := NewStreamReader(conn, aead)
|
||||
|
||||
// 4 & 5. Read Client Request Header
|
||||
reqHeader, err := ReadClientRequestHeader(conn, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
dest := reqHeader.Destination
|
||||
|
||||
// 6. Send Server Response Handshake
|
||||
writer, err := WriteTCPResponse(conn, i.method, userPSK, saltSlice, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 7. Dispatch Connection to Xray routing with matched User
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = user
|
||||
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
|
||||
errors.LogInfo(ctx, "tunneling request to ", dest, " for user ", user.Email)
|
||||
|
||||
link, err := dispatcher.Dispatch(ctx, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(reqHeader.EarlyData) > 0 {
|
||||
earlyBuf := buf.New()
|
||||
earlyBuf.Write(reqHeader.EarlyData)
|
||||
if err := link.Writer.WriteMultiBuffer(buf.MultiBuffer{earlyBuf}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sessionPolicy = i.policyManager.ForLevel(user.Level)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
udpConns := utils.NewTypedSyncMap[uint64, *udpConnEntry]()
|
||||
defer func() {
|
||||
udpConns.Range(func(key uint64, entry *udpConnEntry) bool {
|
||||
entry.timer.SetTimeout(0)
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
reader := buf.NewReader(conn)
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, b := range mb {
|
||||
// In multi-user UDP:
|
||||
// Packet header is 16 bytes: Encrypted(SessionID + PacketID)
|
||||
// Followed by 16 bytes EIH
|
||||
packetBytes := b.Bytes()
|
||||
if len(packetBytes) < 32+1+8+2 {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
for _, buffer := range mb {
|
||||
packet := B.As(buffer.Bytes()).ToOwned()
|
||||
buffer.Release()
|
||||
err = i.service.NewPacket(ctx, pc, packet, metadata)
|
||||
|
||||
var rawHeader [16]byte
|
||||
i.udpMasterCipher.Decrypt(rawHeader[:], packetBytes[:16])
|
||||
|
||||
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
|
||||
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
|
||||
|
||||
// Replay protection & session lookup
|
||||
sessionItem, _ := i.udpSessions.GetOrCreate(sessionID)
|
||||
|
||||
sessionItem.Lock()
|
||||
if !sessionItem.Window.Check(packetID) {
|
||||
sessionItem.Unlock()
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
var userPSK []byte
|
||||
var currentUser *protocol.MemoryUser
|
||||
if sessionItem.User != nil {
|
||||
currentUser = sessionItem.User
|
||||
userPSK = sessionItem.UserPSK
|
||||
sessionItem.Unlock()
|
||||
} else {
|
||||
sessionItem.Unlock()
|
||||
// Decrypt EIH
|
||||
identitySubkey := DeriveIdentitySubKey(i.masterPSK, rawHeader[:8], i.method.KeySaltLength)
|
||||
idBlock, err := i.method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
packet.Release()
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
var decryptedHash [16]byte
|
||||
idBlock.Decrypt(decryptedHash[:], packetBytes[16:32])
|
||||
|
||||
user, ok := i.usersByHash.Load(decryptedHash)
|
||||
if !ok || user == nil {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
currentUser = user
|
||||
userPSK = user.Account.(*MemoryAccount).Key
|
||||
|
||||
sessionItem.Lock()
|
||||
sessionItem.User = user
|
||||
sessionItem.UserPSK = userPSK
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
|
||||
// Decrypt Body (with AEAD caching per session)
|
||||
bodyAead := sessionItem.GetRemoteCipher()
|
||||
if bodyAead == nil {
|
||||
bodyKey := DeriveSessionSubKey(userPSK, rawHeader[:8], i.method.KeySaltLength)
|
||||
var err error
|
||||
bodyAead, err = i.method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
sessionItem.SetRemoteCipher(bodyAead)
|
||||
}
|
||||
|
||||
bodyNonce := rawHeader[4:16]
|
||||
bodyCipher := packetBytes[32:]
|
||||
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
|
||||
b.Release()
|
||||
if err != nil || len(bodyPlain) < 1+8+2 {
|
||||
continue
|
||||
}
|
||||
|
||||
sessionItem.Lock()
|
||||
sessionItem.Window.Add(packetID)
|
||||
sessionItem.Unlock()
|
||||
|
||||
if bodyPlain[0] != HeaderTypeClient {
|
||||
continue
|
||||
}
|
||||
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
|
||||
diff := time.Now().Unix() - int64(epoch)
|
||||
if diff < -30 || diff > 30 {
|
||||
continue
|
||||
}
|
||||
|
||||
paddingLen := int(binary.BigEndian.Uint16(bodyPlain[9:11]))
|
||||
offset := 11 + paddingLen
|
||||
if len(bodyPlain) < offset {
|
||||
continue
|
||||
}
|
||||
|
||||
dest, addrLen, err := parseAddressPort(bodyPlain[offset:])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
payload := bodyPlain[offset+addrLen:]
|
||||
payloadCopy := make([]byte, len(payload))
|
||||
copy(payloadCopy, payload)
|
||||
|
||||
entry, ok := udpConns.Load(sessionID)
|
||||
if !ok {
|
||||
sessCtx, cancel := context.WithCancel(ctx)
|
||||
inbound := session.InboundFromContext(sessCtx)
|
||||
inbound.User = currentUser
|
||||
|
||||
sessCtx = log.ContextWithAccessMessage(sessCtx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: currentUser.Email,
|
||||
})
|
||||
|
||||
link, err := dispatcher.Dispatch(sessCtx, dest)
|
||||
if err != nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
newEntry := &udpConnEntry{
|
||||
link: link,
|
||||
cancel: cancel,
|
||||
}
|
||||
sessionPolicy := i.policyManager.ForLevel(currentUser.Level)
|
||||
newEntry.timer = signal.CancelAfterInactivity(sessCtx, func() {
|
||||
udpConns.Delete(sessionID)
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
cancel()
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
actual, loaded := udpConns.LoadOrStore(sessionID, newEntry)
|
||||
if loaded {
|
||||
newEntry.timer.SetTimeout(0)
|
||||
entry = actual
|
||||
} else {
|
||||
entry = newEntry
|
||||
go func(sessID uint64, uPSK []byte, d net.Destination, cEntry *udpConnEntry) {
|
||||
defer func() {
|
||||
cEntry.timer.SetTimeout(0)
|
||||
}()
|
||||
for {
|
||||
resMb, err := cEntry.link.Reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cEntry.timer.Update()
|
||||
for _, rb := range resMb {
|
||||
encPacket, err := i.encodeServerUDPPacket(sessID, uPSK, d, rb.Bytes())
|
||||
rb.Release()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = conn.Write(encPacket)
|
||||
}
|
||||
}
|
||||
}(sessionID, userPSK, dest, entry)
|
||||
}
|
||||
}
|
||||
|
||||
entry.timer.Update()
|
||||
pBuf := buf.New()
|
||||
pBuf.Write(payloadCopy)
|
||||
_ = entry.link.Writer.WriteMultiBuffer(buf.MultiBuffer{pBuf})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.users[userInt]
|
||||
inbound.User = user
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP)
|
||||
if err != nil {
|
||||
return err
|
||||
func (i *MultiUserInbound) encodeServerUDPPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
sessionItem, _ := i.udpSessions.GetOrCreate(clientSessionID)
|
||||
if err := sessionItem.EnsureServerState(i.method, i.udpMasterCipher, nil, userPSK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return singbridge.CopyConn(ctx, conn, link, conn)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.users[userInt]
|
||||
inbound.User = user
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outConn := &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
return bufio.CopyPacketConn(ctx, conn, outConn)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) NewError(ctx context.Context, err error) {
|
||||
if E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
errors.LogWarning(ctx, err.Error())
|
||||
return sessionItem.EncodeServerPacket(i.method, clientSessionID, dest, payload)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,12 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
A "github.com/sagernet/sing/common/auth"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -22,8 +16,11 @@ import (
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/common/singbridge"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
@@ -34,10 +31,22 @@ func init() {
|
||||
}))
|
||||
}
|
||||
|
||||
type relayDest struct {
|
||||
destination net.Destination
|
||||
email string
|
||||
level uint32
|
||||
key []byte
|
||||
blockCipher cipher.Block
|
||||
}
|
||||
|
||||
type RelayInbound struct {
|
||||
networks []net.Network
|
||||
destinations []*RelayDestination
|
||||
service *shadowaead_2022.RelayService[int]
|
||||
networks []net.Network
|
||||
method *CipherMethod
|
||||
relayPSK []byte
|
||||
relayBlock cipher.Block
|
||||
destinations map[[AESBlockSize]byte]*relayDest
|
||||
rawDestinations []*RelayDestination
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewRelayServer(ctx context.Context, config *RelayServerConfig) (*RelayInbound, error) {
|
||||
@@ -48,39 +57,63 @@ func NewRelayServer(ctx context.Context, config *RelayServerConfig) (*RelayInbou
|
||||
net.Network_UDP,
|
||||
}
|
||||
}
|
||||
inbound := &RelayInbound{
|
||||
networks: networks,
|
||||
destinations: config.Destinations,
|
||||
}
|
||||
if !C.Contains(shadowaead_2022.List, config.Method) || !strings.Contains(config.Method, "aes") {
|
||||
return nil, errors.New("unsupported method ", config.Method)
|
||||
}
|
||||
service, err := shadowaead_2022.NewRelayServiceWithPassword[int](config.Method, config.Key, 500, inbound)
|
||||
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
return nil, err
|
||||
}
|
||||
if method.IsChaCha {
|
||||
return nil, errors.New("shadowsocks 2022 relay: only aes methods are supported")
|
||||
}
|
||||
|
||||
for i, destination := range config.Destinations {
|
||||
if destination.Email == "" {
|
||||
relayPSK, err := ParseKey(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relayBlock, err := method.NewBlock(relayPSK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
i := &RelayInbound{
|
||||
networks: networks,
|
||||
method: method,
|
||||
relayPSK: relayPSK,
|
||||
relayBlock: relayBlock,
|
||||
destinations: make(map[[AESBlockSize]byte]*relayDest),
|
||||
rawDestinations: config.Destinations,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}
|
||||
|
||||
for idx, d := range config.Destinations {
|
||||
if d.Email == "" {
|
||||
u := uuid.New()
|
||||
destination.Email = "unnamed-destination-" + strconv.Itoa(i) + "-" + u.String()
|
||||
d.Email = "unnamed-destination-" + strconv.Itoa(idx) + "-" + u.String()
|
||||
}
|
||||
destKey, err := ParseKey(d.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
destBlock, err := method.NewBlock(destKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash := DeriveUserPSKHash(destKey)
|
||||
|
||||
i.destinations[hash] = &relayDest{
|
||||
destination: net.TCPDestination(d.Address.AsAddress(), net.Port(d.Port)),
|
||||
email: d.Email,
|
||||
level: uint32(d.Level),
|
||||
key: destKey,
|
||||
blockCipher: destBlock,
|
||||
}
|
||||
}
|
||||
err = service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(config.Destinations, func(index int, it *RelayDestination) int { return index }),
|
||||
C.Map(config.Destinations, func(it *RelayDestination) string { return it.Key }),
|
||||
C.Map(config.Destinations, func(it *RelayDestination) M.Socksaddr {
|
||||
return singbridge.ToSocksaddr(net.Destination{
|
||||
Address: it.Address.AsAddress(),
|
||||
Port: net.Port(it.Port),
|
||||
})
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
}
|
||||
inbound.service = service
|
||||
return inbound, nil
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *RelayInbound) Network() []net.Network {
|
||||
@@ -92,103 +125,206 @@ func (i *RelayInbound) Process(ctx context.Context, network net.Network, connect
|
||||
inbound.Name = "shadowsocks-2022-relay"
|
||||
inbound.CanSpliceCopy = 3
|
||||
|
||||
var metadata M.Metadata
|
||||
if inbound.Source.IsValid() {
|
||||
metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr())
|
||||
if network == net.Network_TCP {
|
||||
return i.processTCP(ctx, connection, dispatcher)
|
||||
}
|
||||
return i.processUDP(ctx, connection, dispatcher)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) processTCP(ctx context.Context, conn net.Conn, dispatcher routing.Dispatcher) error {
|
||||
defer conn.Close()
|
||||
|
||||
sessionPolicy := i.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
ctx = session.ContextWithDispatcher(ctx, dispatcher)
|
||||
// Read Salt + Outer EIH
|
||||
needed := i.method.KeySaltLength + AESBlockSize
|
||||
var headerBuf [48]byte
|
||||
headerSlice := headerBuf[:needed]
|
||||
if _, err := io.ReadFull(conn, headerSlice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if network == net.Network_TCP {
|
||||
return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
|
||||
} else {
|
||||
reader := buf.NewReader(connection)
|
||||
pc := &natPacketConn{connection}
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return singbridge.ReturnError(err)
|
||||
salt := headerSlice[:i.method.KeySaltLength]
|
||||
eih := headerSlice[i.method.KeySaltLength:]
|
||||
|
||||
identitySubkey := DeriveIdentitySubKey(i.relayPSK, salt, i.method.KeySaltLength)
|
||||
block, err := i.method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var decryptedHash [AESBlockSize]byte
|
||||
block.Decrypt(decryptedHash[:], eih)
|
||||
|
||||
targetDest, ok := i.destinations[decryptedHash]
|
||||
if !ok {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: targetDest.email,
|
||||
Level: targetDest.level,
|
||||
}
|
||||
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: targetDest.destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: targetDest.email,
|
||||
})
|
||||
|
||||
errors.LogInfo(ctx, "relaying connection to ", targetDest.destination)
|
||||
|
||||
link, err := dispatcher.Dispatch(ctx, targetDest.destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unwrap outer EIH: send client salt to next hop, stripping this hop's EIH
|
||||
saltBuf := buf.New()
|
||||
saltBuf.Write(salt)
|
||||
if err := link.Writer.WriteMultiBuffer(buf.MultiBuffer{saltBuf}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessionPolicy = i.policyManager.ForLevel(targetDest.level)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
udpConns := utils.NewTypedSyncMap[uint64, *udpConnEntry]()
|
||||
defer func() {
|
||||
udpConns.Range(func(key uint64, entry *udpConnEntry) bool {
|
||||
entry.timer.SetTimeout(0)
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
reader := buf.NewReader(conn)
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, b := range mb {
|
||||
data := b.Bytes()
|
||||
if len(data) < 2*AESBlockSize {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
for _, buffer := range mb {
|
||||
packet := B.As(buffer.Bytes()).ToOwned()
|
||||
buffer.Release()
|
||||
err = i.service.NewPacket(ctx, pc, packet, metadata)
|
||||
|
||||
var packetHeader [AESBlockSize]byte
|
||||
i.relayBlock.Decrypt(packetHeader[:], data[:AESBlockSize])
|
||||
|
||||
var eiHeader [AESBlockSize]byte
|
||||
i.relayBlock.Decrypt(eiHeader[:], data[AESBlockSize:2*AESBlockSize])
|
||||
for idx := 0; idx < AESBlockSize; idx++ {
|
||||
eiHeader[idx] ^= packetHeader[idx]
|
||||
}
|
||||
|
||||
targetDest, ok := i.destinations[eiHeader]
|
||||
if !ok {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract sessionID from raw packetHeader for session-level link caching before re-encrypting
|
||||
sessionID := binary.BigEndian.Uint64(packetHeader[:8])
|
||||
|
||||
// Re-encrypt packetHeader with next hop block cipher
|
||||
targetDest.blockCipher.Encrypt(packetHeader[:], packetHeader[:])
|
||||
|
||||
// Strip outer EIH: replace second block with re-encrypted packetHeader and advance
|
||||
copy(data[AESBlockSize:2*AESBlockSize], packetHeader[:])
|
||||
b.Advance(int32(AESBlockSize))
|
||||
|
||||
dest := targetDest.destination
|
||||
dest.Network = net.Network_UDP
|
||||
|
||||
entry, ok := udpConns.Load(sessionID)
|
||||
if !ok {
|
||||
sessCtx, cancel := context.WithCancel(ctx)
|
||||
inbound := session.InboundFromContext(sessCtx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: targetDest.email,
|
||||
Level: targetDest.level,
|
||||
}
|
||||
|
||||
sessCtx = log.ContextWithAccessMessage(sessCtx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: targetDest.email,
|
||||
})
|
||||
|
||||
link, err := dispatcher.Dispatch(sessCtx, dest)
|
||||
if err != nil {
|
||||
packet.Release()
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
cancel()
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
newEntry := &udpConnEntry{
|
||||
link: link,
|
||||
cancel: cancel,
|
||||
}
|
||||
sessionPolicy := i.policyManager.ForLevel(targetDest.level)
|
||||
newEntry.timer = signal.CancelAfterInactivity(sessCtx, func() {
|
||||
udpConns.Delete(sessionID)
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
cancel()
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
actual, loaded := udpConns.LoadOrStore(sessionID, newEntry)
|
||||
if loaded {
|
||||
newEntry.timer.SetTimeout(0)
|
||||
entry = actual
|
||||
} else {
|
||||
entry = newEntry
|
||||
go func(cEntry *udpConnEntry) {
|
||||
defer func() {
|
||||
cEntry.timer.SetTimeout(0)
|
||||
}()
|
||||
for {
|
||||
resMb, err := cEntry.link.Reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cEntry.timer.Update()
|
||||
for _, rb := range resMb {
|
||||
_, _ = conn.Write(rb.Bytes())
|
||||
rb.Release()
|
||||
}
|
||||
}
|
||||
}(entry)
|
||||
}
|
||||
}
|
||||
|
||||
entry.timer.Update()
|
||||
_ = entry.link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *RelayInbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.destinations[userInt]
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: user.Email,
|
||||
Level: uint32(user.Level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return singbridge.CopyConn(ctx, nil, link, conn)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.destinations[userInt]
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: user.Email,
|
||||
Level: uint32(user.Level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outConn := &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
return bufio.CopyPacketConn(ctx, conn, outConn)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) NewError(ctx context.Context, err error) {
|
||||
if E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
errors.LogWarning(ctx, err.Error())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
const (
|
||||
ContextSessionSubKey = "shadowsocks 2022 session subkey"
|
||||
ContextIdentitySubKey = "shadowsocks 2022 identity subkey"
|
||||
)
|
||||
|
||||
// ParseKey decodes a base64 or raw PSK key string and validates its length
|
||||
func ParseKey(key string, keyLength int) ([]byte, error) {
|
||||
raw, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
raw = []byte(key)
|
||||
}
|
||||
if len(raw) != keyLength {
|
||||
return nil, ErrBadKey
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func ParsePSKList(password string, keyLength int) ([][]byte, error) {
|
||||
parts := strings.Split(password, ":")
|
||||
pskList := make([][]byte, len(parts))
|
||||
for i, part := range parts {
|
||||
norm, err := ParseKey(part, keyLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pskList[i] = norm
|
||||
}
|
||||
return pskList, nil
|
||||
}
|
||||
|
||||
func deriveSubKey(ctx string, psk, salt []byte, keyLength int) []byte {
|
||||
var keyMaterial [64]byte
|
||||
kmLen := len(psk) + len(salt)
|
||||
copy(keyMaterial[:], psk)
|
||||
copy(keyMaterial[len(psk):], salt)
|
||||
out := make([]byte, keyLength)
|
||||
blake3.DeriveKey(out, ctx, keyMaterial[:kmLen])
|
||||
return out
|
||||
}
|
||||
|
||||
func DeriveSessionSubKey(psk, salt []byte, keyLength int) []byte {
|
||||
return deriveSubKey(ContextSessionSubKey, psk, salt, keyLength)
|
||||
}
|
||||
|
||||
func DeriveIdentitySubKey(psk, salt []byte, keyLength int) []byte {
|
||||
return deriveSubKey(ContextIdentitySubKey, psk, salt, keyLength)
|
||||
}
|
||||
|
||||
func DeriveUserPSKHash(userPSK []byte) [AESBlockSize]byte {
|
||||
h := blake3.Sum512(userPSK)
|
||||
var out [AESBlockSize]byte
|
||||
copy(out[:], h[:AESBlockSize])
|
||||
return out
|
||||
}
|
||||
@@ -2,21 +2,20 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
shadowsocks "github.com/sagernet/sing-shadowsocks"
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/retry"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/common/singbridge"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
@@ -28,42 +27,49 @@ func init() {
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
ctx context.Context
|
||||
server net.Destination
|
||||
method shadowsocks.Method
|
||||
ctx context.Context
|
||||
server net.Destination
|
||||
method *CipherMethod
|
||||
pskList [][]byte
|
||||
finalPSK []byte
|
||||
udpCodec *UDPPacketCodec
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
|
||||
o := &Outbound{
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, errors.New("unsupported method: ", config.Method).Base(err)
|
||||
}
|
||||
|
||||
pskList, err := ParsePSKList(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid key: ", config.Key).Base(err)
|
||||
}
|
||||
|
||||
finalPSK := pskList[len(pskList)-1]
|
||||
udpCodec, err := NewUDPPacketCodec(method, finalPSK)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to create udp packet codec").Base(err)
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
return &Outbound{
|
||||
ctx: ctx,
|
||||
server: net.Destination{
|
||||
Address: config.Address.AsAddress(),
|
||||
Port: net.Port(config.Port),
|
||||
Network: net.Network_TCP,
|
||||
},
|
||||
}
|
||||
if C.Contains(shadowaead_2022.List, config.Method) {
|
||||
if config.Key == "" {
|
||||
return nil, errors.New("missing psk")
|
||||
}
|
||||
method, err := shadowaead_2022.NewWithPassword(config.Method, config.Key, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("create method").Base(err)
|
||||
}
|
||||
o.method = method
|
||||
} else {
|
||||
return nil, errors.New("unknown method ", config.Method)
|
||||
}
|
||||
return o, nil
|
||||
method: method,
|
||||
pskList: pskList,
|
||||
finalPSK: finalPSK,
|
||||
udpCodec: udpCodec,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
|
||||
var inboundConn net.Conn
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
if inbound != nil {
|
||||
inboundConn = inbound.Conn
|
||||
}
|
||||
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
if !ob.Target.IsValid() {
|
||||
@@ -78,70 +84,123 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
|
||||
|
||||
serverDestination := o.server
|
||||
serverDestination.Network = network
|
||||
connection, err := dialer.Dial(ctx, serverDestination)
|
||||
if err != nil {
|
||||
return errors.New("failed to connect to server").Base(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
var conn net.Conn
|
||||
if err := retry.ExponentialBackoff(5, 100).On(func() error {
|
||||
rawConn, err := dialer.Dial(ctx, serverDestination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn = rawConn
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.New("failed to find an available destination").Base(err).AtWarning()
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var newCtx context.Context
|
||||
var newCancel context.CancelFunc
|
||||
if session.TimeoutOnlyFromContext(ctx) {
|
||||
ctx, _ = context.WithCancel(context.Background())
|
||||
newCtx, newCancel = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
sessionPolicy := o.policyManager.ForLevel(0)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, func() {
|
||||
cancel()
|
||||
if newCancel != nil {
|
||||
newCancel()
|
||||
}
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
if newCtx != nil {
|
||||
ctx = newCtx
|
||||
}
|
||||
|
||||
if network == net.Network_TCP {
|
||||
serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination))
|
||||
var handshake bool
|
||||
if timeoutReader, isTimeoutReader := link.Reader.(buf.TimeoutReader); isTimeoutReader {
|
||||
mb, err := timeoutReader.ReadMultiBufferTimeout(time.Millisecond * 100)
|
||||
if err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
|
||||
return errors.New("read payload").Base(err)
|
||||
}
|
||||
payload := B.New()
|
||||
for {
|
||||
payload.Reset()
|
||||
nb, n := buf.SplitBytes(mb, payload.FreeBytes())
|
||||
if n > 0 {
|
||||
payload.Truncate(n)
|
||||
_, err = serverConn.Write(payload.Bytes())
|
||||
if err != nil {
|
||||
payload.Release()
|
||||
return errors.New("write payload").Base(err)
|
||||
}
|
||||
handshake = true
|
||||
}
|
||||
if nb.IsEmpty() {
|
||||
break
|
||||
}
|
||||
mb = nb
|
||||
}
|
||||
payload.Release()
|
||||
}
|
||||
if !handshake {
|
||||
_, err = serverConn.Write(nil)
|
||||
if err != nil {
|
||||
return errors.New("client handshake").Base(err)
|
||||
}
|
||||
}
|
||||
return singbridge.CopyConn(ctx, inboundConn, link, serverConn)
|
||||
} else {
|
||||
var packetConn N.PacketConn
|
||||
if pc, isPacketConn := inboundConn.(N.PacketConn); isPacketConn {
|
||||
packetConn = pc
|
||||
} else if nc, isNetPacket := inboundConn.(net.PacketConn); isNetPacket {
|
||||
packetConn = bufio.NewPacketConn(nc)
|
||||
} else {
|
||||
packetConn = &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Conn: inboundConn,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
var clientSalt [32]byte
|
||||
clientSaltSlice := clientSalt[:o.method.KeySaltLength]
|
||||
if _, err := io.ReadFull(rand.Reader, clientSaltSlice); err != nil {
|
||||
return errors.New("failed to generate client salt").Base(err)
|
||||
}
|
||||
|
||||
serverConn := o.method.DialPacketConn(connection)
|
||||
return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn))
|
||||
bodyWriter, err := WriteTCPRequest(bufferedWriter, o.method, o.pskList, destination, clientSaltSlice, nil)
|
||||
if err != nil {
|
||||
return errors.New("failed to write request").Base(err)
|
||||
}
|
||||
|
||||
if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
|
||||
return errors.New("failed to write A request payload").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
if err := bufferedWriter.SetBuffered(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
|
||||
responseReader, err := ReadTCPResponse(conn, o.method, o.finalPSK, clientSaltSlice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if network == net.Network_UDP {
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
|
||||
writer := &UDPWriter{
|
||||
Writer: conn,
|
||||
Destination: destination,
|
||||
Codec: o.udpCodec,
|
||||
}
|
||||
|
||||
if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil {
|
||||
return errors.New("failed to transport all UDP request").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
|
||||
reader := &UDPReader{
|
||||
Reader: conn,
|
||||
Codec: o.udpCodec,
|
||||
}
|
||||
|
||||
if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil {
|
||||
return errors.New("failed to transport all UDP response").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("unsupported network: ", network)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
mrand "math/rand/v2"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
type UDPCodec struct {
|
||||
method *CipherMethod
|
||||
psk []byte
|
||||
blockCipher cipher.Block
|
||||
chachaCipher cipher.AEAD
|
||||
clientBodyCipher cipher.AEAD
|
||||
clientSessionID uint64
|
||||
nextPacketID atomic.Uint64
|
||||
sessions *UDPSessionManager
|
||||
}
|
||||
|
||||
type (
|
||||
UDPPacketCodec = UDPCodec
|
||||
UDPServerCodec = UDPCodec
|
||||
)
|
||||
|
||||
func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
|
||||
c := &UDPCodec{
|
||||
method: method,
|
||||
psk: psk,
|
||||
}
|
||||
var err error
|
||||
if method.IsChaCha {
|
||||
c.chachaCipher, err = method.NewUDPCipher(psk)
|
||||
} else {
|
||||
c.blockCipher, err = method.NewBlock(psk)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewUDPPacketCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
|
||||
c, err := newUDPCodec(method, psk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sessID [8]byte
|
||||
if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.clientSessionID = binary.BigEndian.Uint64(sessID[:])
|
||||
|
||||
if !method.IsChaCha {
|
||||
clientBodyKey := DeriveSessionSubKey(psk, sessID[:], method.KeySaltLength)
|
||||
c.clientBodyCipher, err = method.NewAEAD(clientBodyKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewUDPServerCodec(method *CipherMethod, psk []byte, sessionTimeout time.Duration) (*UDPCodec, error) {
|
||||
c, err := newUDPCodec(method, psk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.sessions = NewUDPSessionManager(sessionTimeout)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *UDPCodec) EncodeClientPacket(dest net.Destination, payload []byte) (*buf.Buffer, error) {
|
||||
packetID := c.nextPacketID.Add(1)
|
||||
sessID := c.clientSessionID
|
||||
|
||||
// Padding determination (e.g. DNS port 53 disguise)
|
||||
var paddingLen int
|
||||
if dest.Port == 53 && len(payload) < MaxPaddingLength {
|
||||
paddingLen = mrand.IntN(MaxPaddingLength-len(payload)) + 1
|
||||
}
|
||||
|
||||
addrPortLen := AddrPortLength(dest)
|
||||
|
||||
if c.method.IsChaCha {
|
||||
// ChaCha20 mode: 24-byte nonce + plaintext header (27B) + padding + dest + payload + AEAD tag (16B)
|
||||
totalLen := PacketNonceSize + 27 + paddingLen + addrPortLen + len(payload) + AEADTagSize
|
||||
if totalLen > buf.Size {
|
||||
return nil, ErrPacketTooLarge
|
||||
}
|
||||
|
||||
outBuf := buf.New()
|
||||
|
||||
var nonce [PacketNonceSize]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
outBuf.Release()
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(nonce[:])
|
||||
|
||||
var hdr [16 + 1 + 8 + 2]byte
|
||||
binary.BigEndian.PutUint64(hdr[0:8], sessID)
|
||||
binary.BigEndian.PutUint64(hdr[8:16], packetID)
|
||||
hdr[16] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(hdr[17:25], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(hdr[25:27], uint16(paddingLen))
|
||||
outBuf.Write(hdr[:])
|
||||
if paddingLen > 0 {
|
||||
outBuf.Write(zeroPadding[:paddingLen])
|
||||
}
|
||||
|
||||
if err := WriteAddressPort(outBuf, dest); err != nil {
|
||||
outBuf.Release()
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(payload)
|
||||
|
||||
plainBytes := outBuf.Bytes()[PacketNonceSize:]
|
||||
outBuf.Extend(int32(c.chachaCipher.Overhead()))
|
||||
c.chachaCipher.Seal(plainBytes[:0], nonce[:], plainBytes, nil)
|
||||
return outBuf, nil
|
||||
}
|
||||
|
||||
// AES mode:
|
||||
// 16B Encrypted Header + (11B header + padding + dest + payload + 16B AEAD tag)
|
||||
totalLen := 16 + 11 + paddingLen + addrPortLen + len(payload) + AEADTagSize
|
||||
if totalLen > buf.Size {
|
||||
return nil, ErrPacketTooLarge
|
||||
}
|
||||
|
||||
outBuf := buf.New()
|
||||
|
||||
var rawHeader [16]byte
|
||||
binary.BigEndian.PutUint64(rawHeader[:8], sessID)
|
||||
binary.BigEndian.PutUint64(rawHeader[8:16], packetID)
|
||||
|
||||
var encryptedHeader [16]byte
|
||||
c.blockCipher.Encrypt(encryptedHeader[:], rawHeader[:])
|
||||
outBuf.Write(encryptedHeader[:])
|
||||
|
||||
bodyAead := c.clientBodyCipher
|
||||
|
||||
var hdr [1 + 8 + 2]byte
|
||||
hdr[0] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(hdr[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(hdr[9:11], uint16(paddingLen))
|
||||
outBuf.Write(hdr[:])
|
||||
if paddingLen > 0 {
|
||||
outBuf.Write(zeroPadding[:paddingLen])
|
||||
}
|
||||
|
||||
if err := WriteAddressPort(outBuf, dest); err != nil {
|
||||
outBuf.Release()
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(payload)
|
||||
|
||||
plainBytes := outBuf.Bytes()[16:]
|
||||
bodyNonce := rawHeader[4:16]
|
||||
outBuf.Extend(int32(bodyAead.Overhead()))
|
||||
bodyAead.Seal(plainBytes[:0], bodyNonce, plainBytes, nil)
|
||||
return outBuf, nil
|
||||
}
|
||||
|
||||
type DecodedUDPPacket struct {
|
||||
SessionID uint64
|
||||
PacketID uint64
|
||||
HeaderType byte
|
||||
Timestamp uint64
|
||||
Destination net.Destination
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func parseAddressPort(data []byte) (net.Destination, int, error) {
|
||||
if len(data) < 1 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
switch data[0] {
|
||||
case 1: // IPv4
|
||||
if len(data) < 1+4+2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
ip := net.IPAddress(data[1:5])
|
||||
port := binary.BigEndian.Uint16(data[5:7])
|
||||
return net.UDPDestination(ip, net.Port(port)), 7, nil
|
||||
case 4: // IPv6
|
||||
if len(data) < 1+16+2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
ip := net.IPAddress(data[1:17])
|
||||
port := binary.BigEndian.Uint16(data[17:19])
|
||||
return net.UDPDestination(ip, net.Port(port)), 19, nil
|
||||
case 3: // Domain
|
||||
if len(data) < 2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
domainLen := int(data[1])
|
||||
if len(data) < 2+domainLen+2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
domain := string(data[2 : 2+domainLen])
|
||||
port := binary.BigEndian.Uint16(data[2+domainLen : 2+domainLen+2])
|
||||
return net.UDPDestination(net.DomainAddress(domain), net.Port(port)), 2 + domainLen + 2, nil
|
||||
default:
|
||||
return net.Destination{}, 0, errors.New("unknown address type")
|
||||
}
|
||||
}
|
||||
|
||||
func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedUDPPacket, error) {
|
||||
if len(bodyPlain) < 1+8+2 {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
|
||||
headerType := bodyPlain[0]
|
||||
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
if diff > 30 {
|
||||
return DecodedUDPPacket{}, ErrBadTimestamp
|
||||
}
|
||||
|
||||
offset := 9
|
||||
if headerType == HeaderTypeServer {
|
||||
if len(bodyPlain) < offset+8+2 {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
offset += 8 // skip clientSessionID
|
||||
}
|
||||
|
||||
paddingLen := int(binary.BigEndian.Uint16(bodyPlain[offset : offset+2]))
|
||||
offset += 2
|
||||
|
||||
if len(bodyPlain) < offset+paddingLen {
|
||||
return DecodedUDPPacket{}, ErrNoPadding
|
||||
}
|
||||
offset += paddingLen
|
||||
|
||||
dest, addrLen, err := parseAddressPort(bodyPlain[offset:])
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, err
|
||||
}
|
||||
payload := bodyPlain[offset+addrLen:]
|
||||
|
||||
return DecodedUDPPacket{
|
||||
SessionID: sessionID,
|
||||
PacketID: packetID,
|
||||
HeaderType: headerType,
|
||||
Timestamp: epoch,
|
||||
Destination: dest,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) {
|
||||
if len(data) < PacketMinimalHeaderSize {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
|
||||
if c.method.IsChaCha {
|
||||
if len(data) < PacketNonceSize+AEADTagSize {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
nonce := data[:PacketNonceSize]
|
||||
ciphertext := data[PacketNonceSize:]
|
||||
plain, err := c.chachaCipher.Open(ciphertext[:0], nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err)
|
||||
}
|
||||
if len(plain) < 16+1+8+2 {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
|
||||
sessionID := binary.BigEndian.Uint64(plain[:8])
|
||||
packetID := binary.BigEndian.Uint64(plain[8:16])
|
||||
|
||||
if c.sessions != nil {
|
||||
sessionItem, _ := c.sessions.GetOrCreate(sessionID)
|
||||
sessionItem.Lock()
|
||||
if !sessionItem.Window.CheckAndAdd(packetID) {
|
||||
sessionItem.Unlock()
|
||||
return DecodedUDPPacket{}, ErrPacketIdNotUnique
|
||||
}
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
|
||||
return parsePlainUDPPacket(sessionID, packetID, plain[16:])
|
||||
}
|
||||
|
||||
// AES mode
|
||||
var rawHeader [16]byte
|
||||
c.blockCipher.Decrypt(rawHeader[:], data[:16])
|
||||
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
|
||||
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
|
||||
|
||||
var bodyAead cipher.AEAD
|
||||
var sessionItem *ServerUDPSession
|
||||
|
||||
if c.sessions != nil {
|
||||
sessionItem, _ = c.sessions.GetOrCreate(sessionID)
|
||||
sessionItem.Lock()
|
||||
if !sessionItem.Window.Check(packetID) {
|
||||
sessionItem.Unlock()
|
||||
return DecodedUDPPacket{}, ErrPacketIdNotUnique
|
||||
}
|
||||
sessionItem.Unlock()
|
||||
|
||||
bodyAead = sessionItem.GetRemoteCipher()
|
||||
if bodyAead == nil {
|
||||
bodyKey := DeriveSessionSubKey(c.psk, rawHeader[:8], c.method.KeySaltLength)
|
||||
var err error
|
||||
bodyAead, err = c.method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, err
|
||||
}
|
||||
sessionItem.SetRemoteCipher(bodyAead)
|
||||
}
|
||||
} else {
|
||||
bodyKey := DeriveSessionSubKey(c.psk, rawHeader[:8], c.method.KeySaltLength)
|
||||
var err error
|
||||
bodyAead, err = c.method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, err
|
||||
}
|
||||
}
|
||||
|
||||
bodyNonce := rawHeader[4:16]
|
||||
bodyCipher := data[16:]
|
||||
bodyPlain, err := bodyAead.Open(bodyCipher[:0], bodyNonce, bodyCipher, nil)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
|
||||
}
|
||||
|
||||
if sessionItem != nil {
|
||||
sessionItem.Lock()
|
||||
sessionItem.Window.Add(packetID)
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
|
||||
return parsePlainUDPPacket(sessionID, packetID, bodyPlain)
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, headerBlock cipher.Block, chachaCipher cipher.AEAD, psk []byte) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.ServerSessionID != 0 {
|
||||
return nil
|
||||
}
|
||||
var sidBuf [8]byte
|
||||
for {
|
||||
if _, err := io.ReadFull(rand.Reader, sidBuf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
s.ServerSessionID = binary.BigEndian.Uint64(sidBuf[:])
|
||||
if s.ServerSessionID != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if method.IsChaCha {
|
||||
s.ServerChaCha = chachaCipher
|
||||
} else {
|
||||
s.ServerBlockCipher = headerBlock
|
||||
bodyKey := DeriveSessionSubKey(psk, sidBuf[:], method.KeySaltLength)
|
||||
bodyAead, err := method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
s.ServerSessionID = 0
|
||||
return err
|
||||
}
|
||||
s.ServerCipher = bodyAead
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) EncodeServerPacket(method *CipherMethod, clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
serverSessionID := s.ServerSessionID
|
||||
serverPacketID := s.ServerPacketID.Add(1)
|
||||
|
||||
if method.IsChaCha {
|
||||
var nonce [PacketNonceSize]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainBuf := buf.New()
|
||||
defer plainBuf.Release()
|
||||
|
||||
var hdr [16 + 1 + 8 + 8 + 2]byte
|
||||
binary.BigEndian.PutUint64(hdr[0:8], serverSessionID)
|
||||
binary.BigEndian.PutUint64(hdr[8:16], serverPacketID)
|
||||
hdr[16] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(hdr[17:25], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint64(hdr[25:33], clientSessionID)
|
||||
binary.BigEndian.PutUint16(hdr[33:35], 0)
|
||||
plainBuf.Write(hdr[:])
|
||||
|
||||
if err := WriteAddressPort(plainBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plainBuf.Write(payload)
|
||||
|
||||
sealed := s.ServerChaCha.Seal(nil, nonce[:], plainBuf.Bytes(), nil)
|
||||
res := make([]byte, PacketNonceSize+len(sealed))
|
||||
copy(res[:PacketNonceSize], nonce[:])
|
||||
copy(res[PacketNonceSize:], sealed)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// AES mode
|
||||
var rawHeader [16]byte
|
||||
binary.BigEndian.PutUint64(rawHeader[:8], serverSessionID)
|
||||
binary.BigEndian.PutUint64(rawHeader[8:16], serverPacketID)
|
||||
|
||||
var encryptedHeader [16]byte
|
||||
s.ServerBlockCipher.Encrypt(encryptedHeader[:], rawHeader[:])
|
||||
|
||||
bodyBuf := buf.New()
|
||||
defer bodyBuf.Release()
|
||||
|
||||
var hdr [1 + 8 + 8 + 2]byte
|
||||
hdr[0] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(hdr[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint64(hdr[9:17], clientSessionID)
|
||||
binary.BigEndian.PutUint16(hdr[17:19], 0)
|
||||
bodyBuf.Write(hdr[:])
|
||||
|
||||
if err := WriteAddressPort(bodyBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyBuf.Write(payload)
|
||||
|
||||
bodyNonce := rawHeader[4:16]
|
||||
sealedBody := s.ServerCipher.Seal(nil, bodyNonce, bodyBuf.Bytes(), nil)
|
||||
|
||||
res := make([]byte, 16+len(sealedBody))
|
||||
copy(res[:16], encryptedHeader[:])
|
||||
copy(res[16:], sealedBody)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *UDPCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
sessionItem, _ := c.sessions.GetOrCreate(clientSessionID)
|
||||
if err := sessionItem.EnsureServerState(c.method, c.blockCipher, c.chachaCipher, c.psk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sessionItem.EncodeServerPacket(c.method, clientSessionID, dest, payload)
|
||||
}
|
||||
|
||||
func (c *UDPCodec) EncodePacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
return c.EncodeServerPacket(clientSessionID, dest, payload)
|
||||
}
|
||||
|
||||
type UDPWriter struct {
|
||||
Writer io.Writer
|
||||
Destination net.Destination
|
||||
Codec *UDPPacketCodec
|
||||
}
|
||||
|
||||
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
for {
|
||||
mb2, b := buf.SplitFirst(mb)
|
||||
mb = mb2
|
||||
if b == nil {
|
||||
break
|
||||
}
|
||||
dest := w.Destination
|
||||
if b.UDP != nil {
|
||||
dest = *b.UDP
|
||||
}
|
||||
pktBuf, err := w.Codec.EncodeClientPacket(dest, b.Bytes())
|
||||
b.Release()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
_, writeErr := w.Writer.Write(pktBuf.Bytes())
|
||||
pktBuf.Release()
|
||||
if writeErr != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UDPReader struct {
|
||||
Reader io.Reader
|
||||
Codec *UDPPacketCodec
|
||||
}
|
||||
|
||||
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
for {
|
||||
buffer := buf.New()
|
||||
_, err := buffer.ReadFrom(r.Reader)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoded, err := r.Codec.DecodePacket(buffer.Bytes())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
continue
|
||||
}
|
||||
buffer.Clear()
|
||||
buffer.Write(decoded.Payload)
|
||||
dest := decoded.Destination
|
||||
buffer.UDP = &dest
|
||||
return buf.MultiBuffer{buffer}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package shadowsocks_2022_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
gonet "net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
. "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
// encodeRelayClientUDPPacket encodes a Shadowsocks-2022 UDP packet with 1 layer of EIH (Relay)
|
||||
func encodeRelayClientUDPPacket(relayKey, destKey []byte, sessionID, packetID uint64, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
method, err := GetCipherMethod(MethodAES128GCM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
relayBlock, err := method.NewBlock(relayKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. Plain packet header: sessionID (8B) + packetID (8B)
|
||||
var rawHeader [16]byte
|
||||
binary.BigEndian.PutUint64(rawHeader[:8], sessionID)
|
||||
binary.BigEndian.PutUint64(rawHeader[8:16], packetID)
|
||||
|
||||
// Encrypt packetHeader under relayKey
|
||||
var encPacketHeader [16]byte
|
||||
relayBlock.Encrypt(encPacketHeader[:], rawHeader[:])
|
||||
|
||||
// 2. EI Header: blake3(destKey)[:16] ^ rawHeader
|
||||
var destHash [16]byte
|
||||
hash512 := blake3.Sum512(destKey)
|
||||
copy(destHash[:], hash512[:16])
|
||||
|
||||
var eiHeader [16]byte
|
||||
for i := 0; i < 16; i++ {
|
||||
eiHeader[i] = destHash[i] ^ rawHeader[i]
|
||||
}
|
||||
var encEIHeader [16]byte
|
||||
relayBlock.Encrypt(encEIHeader[:], eiHeader[:])
|
||||
|
||||
// 3. Payload under destination server's AEAD
|
||||
bodyKey := DeriveSessionSubKey(destKey, rawHeader[:8], 16)
|
||||
bodyAead, err := method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyNonce := rawHeader[4:16]
|
||||
|
||||
outBuf := buf.New()
|
||||
defer outBuf.Release()
|
||||
|
||||
// VarHeader: client type (1) + timestamp (8) + paddingLen (2) + padding + dest + payload
|
||||
var hdr [1 + 8 + 2]byte
|
||||
hdr[0] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(hdr[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(hdr[9:11], 0)
|
||||
outBuf.Write(hdr[:])
|
||||
|
||||
if err := WriteAddressPort(outBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(payload)
|
||||
|
||||
plainBytes := outBuf.Bytes()
|
||||
outBuf.Extend(int32(bodyAead.Overhead()))
|
||||
bodyAead.Seal(plainBytes[:0], bodyNonce, plainBytes, nil)
|
||||
|
||||
// Full packet: encPacketHeader (16B) + encEIHeader (16B) + sealedBody
|
||||
packet := make([]byte, 0, 32+outBuf.Len())
|
||||
packet = append(packet, encPacketHeader[:]...)
|
||||
packet = append(packet, encEIHeader[:]...)
|
||||
packet = append(packet, outBuf.Bytes()...)
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
|
||||
relayKey := []byte("0123456789abcdef")
|
||||
destKey := []byte("fedcba9876543210")
|
||||
relayKeyB64 := base64.StdEncoding.EncodeToString(relayKey)
|
||||
destKeyB64 := base64.StdEncoding.EncodeToString(destKey)
|
||||
|
||||
config := &RelayServerConfig{
|
||||
Method: MethodAES128GCM,
|
||||
Key: relayKeyB64,
|
||||
Destinations: []*RelayDestination{
|
||||
{
|
||||
Key: destKeyB64,
|
||||
Address: &net.IPOrDomain{Address: &net.IPOrDomain_Ip{Ip: []byte{127, 0, 0, 1}}},
|
||||
Port: 8388,
|
||||
Email: "dest@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
inbound, err := NewRelayServer(newTestContext(), config)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create RelayServer: %v", err)
|
||||
}
|
||||
|
||||
sessionID := uint64(0x1122334455667788)
|
||||
dest := net.UDPDestination(net.LocalHostIP, 8388)
|
||||
|
||||
pkt1, err := encodeRelayClientUDPPacket(relayKey, destKey, sessionID, 1, dest, []byte("xray packet 1"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode pkt1: %v", err)
|
||||
}
|
||||
pkt2, err := encodeRelayClientUDPPacket(relayKey, destKey, sessionID, 2, dest, []byte("xray packet 2"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode pkt2: %v", err)
|
||||
}
|
||||
|
||||
var dispatchCount atomic.Int32
|
||||
var receivedPackets [][]byte
|
||||
var mu sync.Mutex
|
||||
|
||||
disp := &dummyDispatcher{
|
||||
onDispatch: func(ctx context.Context, d net.Destination) (*transport.Link, error) {
|
||||
dispatchCount.Add(1)
|
||||
linkR, linkW := gonet.Pipe()
|
||||
t.Cleanup(func() {
|
||||
linkW.Close()
|
||||
linkR.Close()
|
||||
})
|
||||
link := &transport.Link{
|
||||
Reader: buf.NewReader(linkR),
|
||||
Writer: &customWriter{
|
||||
write: func(mb buf.MultiBuffer) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, b := range mb {
|
||||
cpy := make([]byte, b.Len())
|
||||
copy(cpy, b.Bytes())
|
||||
receivedPackets = append(receivedPackets, cpy)
|
||||
b.Release()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
return link, nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConn, serverConn := gonet.Pipe()
|
||||
defer clientConn.Close()
|
||||
defer serverConn.Close()
|
||||
|
||||
inboundConn := &dummyStatConn{Conn: serverConn}
|
||||
ctx, cancel := context.WithCancel(newTestContext())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
_ = inbound.Process(ctx, net.Network_UDP, inboundConn, disp)
|
||||
}()
|
||||
|
||||
// Send Packet 1
|
||||
_, err = clientConn.Write(pkt1)
|
||||
if err != nil {
|
||||
t.Fatalf("write pkt1 failed: %v", err)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Send Packet 2 (same sessionID, packetID=2)
|
||||
_, err = clientConn.Write(pkt2)
|
||||
if err != nil {
|
||||
t.Fatalf("write pkt2 failed: %v", err)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Check dispatch count: For the SAME UDP session, Dispatch MUST be called exactly ONCE!
|
||||
if count := dispatchCount.Load(); count != 1 {
|
||||
t.Fatalf("CRITICAL BUG CONFIRMED: expected dispatchCount = 1 for same session, got %d (sessionID was corrupted by Encrypt!)", count)
|
||||
}
|
||||
|
||||
// Verify downstream destination can decode both packets
|
||||
method, err := GetCipherMethod(MethodAES128GCM)
|
||||
common.Must(err)
|
||||
destCodec, err := NewUDPServerCodec(method, destKey, 300*time.Second)
|
||||
common.Must(err)
|
||||
|
||||
mu.Lock()
|
||||
pkts := receivedPackets
|
||||
mu.Unlock()
|
||||
|
||||
if len(pkts) != 2 {
|
||||
t.Fatalf("expected 2 received packets at destination, got %d", len(pkts))
|
||||
}
|
||||
|
||||
dec1, err := destCodec.DecodePacket(pkts[0])
|
||||
if err != nil {
|
||||
t.Fatalf("dest failed to decode packet 1: %v", err)
|
||||
}
|
||||
if dec1.SessionID != sessionID || dec1.PacketID != 1 || string(dec1.Payload) != "xray packet 1" {
|
||||
t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload))
|
||||
}
|
||||
|
||||
dec2, err := destCodec.DecodePacket(pkts[1])
|
||||
if err != nil {
|
||||
t.Fatalf("dest failed to decode packet 2: %v", err)
|
||||
}
|
||||
if dec2.SessionID != sessionID || dec2.PacketID != 2 || string(dec2.Payload) != "xray packet 2" {
|
||||
t.Fatalf("dec2 mismatch: sess=%x, pktID=%d, payload=%s", dec2.SessionID, dec2.PacketID, string(dec2.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
type customWriter struct {
|
||||
write func(mb buf.MultiBuffer) error
|
||||
}
|
||||
|
||||
func (w *customWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
return w.write(mb)
|
||||
}
|
||||
|
||||
func (w *customWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *customWriter) Interrupt() {}
|
||||
@@ -0,0 +1,159 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
swBlockBitLog = 6 // 1<<6 == 64 bits
|
||||
swBlockBits = 1 << swBlockBitLog // 64
|
||||
swRingBlocks = 1 << 7 // 128
|
||||
swBlockMask = swRingBlocks - 1 // 127
|
||||
swBitMask = swBlockBits - 1 // 63
|
||||
swSize = (swRingBlocks - 1) * swBlockBits // 8128
|
||||
)
|
||||
|
||||
type SlidingWindow struct {
|
||||
last uint64
|
||||
ring [swRingBlocks]uint64
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) Reset() {
|
||||
f.last = 0
|
||||
f.ring[0] = 0
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) Check(counter uint64) bool {
|
||||
switch {
|
||||
case counter > f.last:
|
||||
return true
|
||||
case f.last-counter > swSize:
|
||||
return false
|
||||
}
|
||||
|
||||
blockIndex := (counter >> swBlockBitLog) & swBlockMask
|
||||
bitIndex := counter & swBitMask
|
||||
return (f.ring[blockIndex]>>bitIndex)&1 == 0
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) Add(counter uint64) {
|
||||
blockIndex := counter >> swBlockBitLog
|
||||
|
||||
if counter > f.last {
|
||||
lastBlockIndex := f.last >> swBlockBitLog
|
||||
diff := int(blockIndex - lastBlockIndex)
|
||||
if diff > swRingBlocks {
|
||||
diff = swRingBlocks
|
||||
}
|
||||
|
||||
for i := 0; i < diff; i++ {
|
||||
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
|
||||
f.ring[lastBlockIndex] = 0
|
||||
}
|
||||
|
||||
f.last = counter
|
||||
}
|
||||
|
||||
blockIndex &= swBlockMask
|
||||
bitIndex := counter & swBitMask
|
||||
f.ring[blockIndex] |= 1 << bitIndex
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) CheckAndAdd(counter uint64) bool {
|
||||
if !f.Check(counter) {
|
||||
return false
|
||||
}
|
||||
f.Add(counter)
|
||||
return true
|
||||
}
|
||||
|
||||
type ServerUDPSession struct {
|
||||
sync.Mutex
|
||||
SessionID uint64
|
||||
RemoteCipher atomic.Pointer[cipher.AEAD]
|
||||
Window SlidingWindow
|
||||
User *protocol.MemoryUser
|
||||
UserPSK []byte
|
||||
LastActive atomic.Int64 // Unix timestamp in seconds
|
||||
|
||||
ServerSessionID uint64
|
||||
ServerPacketID atomic.Uint64
|
||||
ServerCipher cipher.AEAD
|
||||
ServerBlockCipher cipher.Block
|
||||
ServerChaCha cipher.AEAD
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) GetRemoteCipher() cipher.AEAD {
|
||||
ptr := s.RemoteCipher.Load()
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) SetRemoteCipher(c cipher.AEAD) {
|
||||
s.RemoteCipher.Store(&c)
|
||||
}
|
||||
|
||||
type UDPSessionManager struct {
|
||||
sessions *utils.TypedSyncMap[uint64, *ServerUDPSession]
|
||||
timeout time.Duration
|
||||
lastClean atomic.Int64 // Unix timestamp in seconds
|
||||
}
|
||||
|
||||
func NewUDPSessionManager(timeout time.Duration) *UDPSessionManager {
|
||||
return &UDPSessionManager{
|
||||
sessions: utils.NewTypedSyncMap[uint64, *ServerUDPSession](),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UDPSessionManager) GetOrCreate(sessionID uint64) (*ServerUDPSession, bool) {
|
||||
now := time.Now().Unix()
|
||||
if s, ok := m.sessions.Load(sessionID); ok {
|
||||
s.LastActive.Store(now)
|
||||
return s, true
|
||||
}
|
||||
|
||||
s := &ServerUDPSession{
|
||||
SessionID: sessionID,
|
||||
}
|
||||
s.LastActive.Store(now)
|
||||
|
||||
actual, loaded := m.sessions.LoadOrStore(sessionID, s)
|
||||
if loaded {
|
||||
actual.LastActive.Store(now)
|
||||
return actual, true
|
||||
}
|
||||
|
||||
// Trigger cleanup if at least 30 seconds have passed since last cleanup
|
||||
last := m.lastClean.Load()
|
||||
if now-last > 30 && m.lastClean.CompareAndSwap(last, now) {
|
||||
go m.cleanup(now)
|
||||
}
|
||||
|
||||
return s, false
|
||||
}
|
||||
|
||||
func (m *UDPSessionManager) cleanup(now int64) {
|
||||
timeoutSec := int64(m.timeout.Seconds())
|
||||
if timeoutSec <= 0 {
|
||||
timeoutSec = 60
|
||||
}
|
||||
m.sessions.Range(func(k uint64, v *ServerUDPSession) bool {
|
||||
if now-v.LastActive.Load() > timeoutSec {
|
||||
m.sessions.Delete(k)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *UDPSessionManager) Delete(sessionID uint64) {
|
||||
m.sessions.Delete(sessionID)
|
||||
}
|
||||
@@ -1 +1,66 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
)
|
||||
|
||||
type udpConnEntry struct {
|
||||
sync.Mutex
|
||||
link *transport.Link
|
||||
timer *signal.ActivityTimer
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
const (
|
||||
HeaderTypeClient = 0
|
||||
HeaderTypeServer = 1
|
||||
MaxPaddingLength = 900
|
||||
PacketNonceSize = 24
|
||||
MaxPacketSize = 65535
|
||||
RequestHeaderFixedChunkLength = 1 + 8 + 2 // Type (1B) + Timestamp (8B) + VarHeaderLen (2B)
|
||||
PacketMinimalHeaderSize = 30
|
||||
StreamNonceSize = 12
|
||||
AESBlockSize = 16
|
||||
AEADTagSize = 16
|
||||
)
|
||||
|
||||
var zeroPadding [MaxPaddingLength]byte
|
||||
|
||||
const (
|
||||
MethodAES128GCM = "2022-blake3-aes-128-gcm"
|
||||
MethodAES256GCM = "2022-blake3-aes-256-gcm"
|
||||
MethodChaCha20Poly1305 = "2022-blake3-chacha20-poly1305"
|
||||
)
|
||||
|
||||
var List = []string{
|
||||
MethodAES128GCM,
|
||||
MethodAES256GCM,
|
||||
MethodChaCha20Poly1305,
|
||||
}
|
||||
|
||||
var (
|
||||
ErrBadKey = errors.New("bad key")
|
||||
ErrBadHeaderType = errors.New("bad header type")
|
||||
ErrBadTimestamp = errors.New("bad timestamp")
|
||||
ErrSaltNotUnique = errors.New("salt not unique")
|
||||
ErrPacketIdNotUnique = errors.New("packet id not unique")
|
||||
ErrPacketTooShort = errors.New("packet too short")
|
||||
ErrPacketTooLarge = errors.New("packet too large")
|
||||
ErrNoPadding = errors.New("bad request: missing payload or padding")
|
||||
ErrInvalidRequest = errors.New("invalid request")
|
||||
)
|
||||
|
||||
func IsSupportedMethod(method string) bool {
|
||||
for _, m := range List {
|
||||
if strings.EqualFold(m, method) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,794 @@
|
||||
package shadowsocks_2022_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
gonet "net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/antireplay"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
. "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
|
||||
func newTestContext() context.Context {
|
||||
v, err := core.New(&core.Config{})
|
||||
common.Must(err)
|
||||
ctx := context.WithValue(context.Background(), core.XrayKey(1), v)
|
||||
ctx = session.ContextWithInbound(ctx, &session.Inbound{})
|
||||
return ctx
|
||||
}
|
||||
|
||||
func generateRandomKey(size int) string {
|
||||
b := make([]byte, size)
|
||||
_, _ = rand.Read(b)
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func TestKDF(t *testing.T) {
|
||||
// Test ParseKey
|
||||
if _, err := ParseKey("", 16); err != ErrBadKey {
|
||||
t.Fatalf("expected ErrBadKey for empty key, got %v", err)
|
||||
}
|
||||
|
||||
shortKey := base64.StdEncoding.EncodeToString([]byte("short"))
|
||||
if _, err := ParseKey(shortKey, 16); err != ErrBadKey {
|
||||
t.Fatalf("expected ErrBadKey for short key, got %v", err)
|
||||
}
|
||||
|
||||
exactKey := []byte("0123456789abcdef")
|
||||
exactKeyB64 := base64.StdEncoding.EncodeToString(exactKey)
|
||||
normExact, err := ParseKey(exactKeyB64, 16)
|
||||
if err != nil || !bytes.Equal(normExact, exactKey) {
|
||||
t.Fatalf("unexpected parsed exact key: %v, err: %v", normExact, err)
|
||||
}
|
||||
|
||||
longKey := base64.StdEncoding.EncodeToString([]byte("0123456789abcdef_longer_key_for_testing"))
|
||||
if _, err := ParseKey(longKey, 16); err != ErrBadKey {
|
||||
t.Fatalf("expected ErrBadKey for long key, got %v", err)
|
||||
}
|
||||
|
||||
// Test Session Subkey determinism
|
||||
salt := []byte("random_salt_1234")
|
||||
k1 := DeriveSessionSubKey(normExact, salt, 16)
|
||||
k2 := DeriveSessionSubKey(normExact, salt, 16)
|
||||
if !bytes.Equal(k1, k2) {
|
||||
t.Fatal("DeriveSessionSubKey should be deterministic")
|
||||
}
|
||||
|
||||
// Identity subkey must differ from session subkey with same inputs
|
||||
idKey := DeriveIdentitySubKey(normExact, salt, 16)
|
||||
if bytes.Equal(k1, idKey) {
|
||||
t.Fatal("DeriveIdentitySubKey must differ from DeriveSessionSubKey")
|
||||
}
|
||||
|
||||
// User PSK hash
|
||||
h1 := DeriveUserPSKHash(normExact)
|
||||
h2 := DeriveUserPSKHash(normExact)
|
||||
if h1 != h2 {
|
||||
t.Fatal("DeriveUserPSKHash should be deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayFilter(t *testing.T) {
|
||||
filter := antireplay.NewMapFilter[string](60)
|
||||
|
||||
salt1 := []byte("test_salt_111111")
|
||||
salt2 := []byte("test_salt_222222")
|
||||
|
||||
if !filter.Check(string(salt1)) {
|
||||
t.Fatal("first check on salt1 should be true")
|
||||
}
|
||||
if filter.Check(string(salt1)) {
|
||||
t.Fatal("second check on salt1 should be false (replay detected)")
|
||||
}
|
||||
|
||||
if !filter.Check(string(salt2)) {
|
||||
t.Fatal("first check on salt2 should be true")
|
||||
}
|
||||
|
||||
// Test SlidingWindow
|
||||
var window SlidingWindow
|
||||
if !window.Check(1) {
|
||||
t.Fatal("packet 1 should be accepted")
|
||||
}
|
||||
window.Add(1)
|
||||
|
||||
if window.Check(1) {
|
||||
t.Fatal("duplicate packet 1 should be rejected")
|
||||
}
|
||||
|
||||
if !window.Check(100) {
|
||||
t.Fatal("packet 100 should be accepted")
|
||||
}
|
||||
window.Add(100)
|
||||
|
||||
if window.Check(100) {
|
||||
t.Fatal("duplicate packet 100 should be rejected")
|
||||
}
|
||||
|
||||
if !window.Check(50) {
|
||||
t.Fatal("out-of-order packet 50 within window should be accepted")
|
||||
}
|
||||
window.Add(50)
|
||||
if window.Check(50) {
|
||||
t.Fatal("duplicate packet 50 should be rejected")
|
||||
}
|
||||
|
||||
// Check packet far behind window (> 8128)
|
||||
window.Add(10000)
|
||||
if window.Check(1) {
|
||||
t.Fatal("packet 1 should be rejected as behind window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPStreamAndHandshake(t *testing.T) {
|
||||
methods := []struct {
|
||||
name string
|
||||
keySize int
|
||||
}{
|
||||
{MethodAES128GCM, 16},
|
||||
{MethodAES256GCM, 32},
|
||||
{MethodChaCha20Poly1305, 32},
|
||||
}
|
||||
|
||||
dest := net.TCPDestination(net.LocalHostIP, net.Port(8080))
|
||||
testPayload := []byte("Hello, Shadowsocks 2022 Native Implementation!")
|
||||
|
||||
for _, m := range methods {
|
||||
t.Run(m.name, func(t *testing.T) {
|
||||
rawKey := make([]byte, m.keySize)
|
||||
_, _ = rand.Read(rawKey)
|
||||
method, err := GetCipherMethod(m.name)
|
||||
common.Must(err)
|
||||
|
||||
clientConn, serverConn := gonet.Pipe()
|
||||
defer clientConn.Close()
|
||||
defer serverConn.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
var receivedDest net.Destination
|
||||
var receivedPayload []byte
|
||||
|
||||
// Server goroutine
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
salt := make([]byte, method.KeySaltLength)
|
||||
_, err := io.ReadFull(serverConn, salt)
|
||||
common.Must(err)
|
||||
|
||||
sessionKey := DeriveSessionSubKey(rawKey, salt, method.KeySaltLength)
|
||||
aead, err := method.NewAEAD(sessionKey)
|
||||
common.Must(err)
|
||||
|
||||
reader := NewStreamReader(serverConn, aead)
|
||||
|
||||
// Read fixed chunk (11 + 16 bytes)
|
||||
var fixedBuf [RequestHeaderFixedChunkLength + AEADTagSize]byte
|
||||
_, err = io.ReadFull(serverConn, fixedBuf[:])
|
||||
common.Must(err)
|
||||
|
||||
plainFixed, err := aead.Open(fixedBuf[:0], reader.Nonce(), fixedBuf[:], nil)
|
||||
common.Must(err)
|
||||
IncreaseNonce(reader.Nonce())
|
||||
if plainFixed[0] != HeaderTypeClient {
|
||||
t.Errorf("expected client header type, got %d", plainFixed[0])
|
||||
}
|
||||
|
||||
// Read variable chunk
|
||||
varLen := int(plainFixed[9])<<8 | int(plainFixed[10])
|
||||
varBuf := make([]byte, varLen+AEADTagSize)
|
||||
_, err = io.ReadFull(serverConn, varBuf)
|
||||
common.Must(err)
|
||||
|
||||
plainVar, err := aead.Open(varBuf[:0], reader.Nonce(), varBuf, nil)
|
||||
common.Must(err)
|
||||
IncreaseNonce(reader.Nonce())
|
||||
|
||||
vBuf := buf.New()
|
||||
vBuf.Write(plainVar)
|
||||
receivedDest, err = ReadAddressPort(vBuf)
|
||||
common.Must(err)
|
||||
|
||||
// Skip padding
|
||||
var padBytes [2]byte
|
||||
_, _ = vBuf.Read(padBytes[:])
|
||||
padLen := int(padBytes[0])<<8 | int(padBytes[1])
|
||||
vBuf.Advance(int32(padLen))
|
||||
|
||||
receivedPayload = make([]byte, vBuf.Len())
|
||||
copy(receivedPayload, vBuf.Bytes())
|
||||
vBuf.Release()
|
||||
|
||||
// Server sends response handshake
|
||||
serverSalt := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(serverSalt)
|
||||
respKey := DeriveSessionSubKey(rawKey, serverSalt, method.KeySaltLength)
|
||||
respAead, err := method.NewAEAD(respKey)
|
||||
writer := NewStreamWriter(serverConn, respAead)
|
||||
_, _ = serverConn.Write(serverSalt)
|
||||
|
||||
fixedResp := make([]byte, 1+8+method.KeySaltLength+2)
|
||||
fixedResp[0] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(fixedResp[1:9], uint64(time.Now().Unix()))
|
||||
copy(fixedResp[9:9+method.KeySaltLength], salt)
|
||||
binary.BigEndian.PutUint16(fixedResp[9+method.KeySaltLength:11+method.KeySaltLength], 0)
|
||||
|
||||
fixedChunk := respAead.Seal(nil, writer.Nonce(), fixedResp, nil)
|
||||
IncreaseNonce(writer.Nonce())
|
||||
_, _ = serverConn.Write(fixedChunk)
|
||||
|
||||
// Echo stream data
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
common.Must(err)
|
||||
_ = writer.WriteMultiBuffer(mb)
|
||||
}()
|
||||
|
||||
// Client goroutine
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
clientSalt, writer, err := ClientHandshake(clientConn, method, [][]byte{rawKey}, dest, testPayload)
|
||||
common.Must(err)
|
||||
|
||||
reader, _, err := ClientVerifyServerResponse(clientConn, method, rawKey, clientSalt)
|
||||
common.Must(err)
|
||||
|
||||
// Send additional stream data
|
||||
streamData := []byte("stream chunk test")
|
||||
_ = writer.WriteChunk(streamData)
|
||||
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
common.Must(err)
|
||||
if !bytes.Equal(mb[0].Bytes(), streamData) {
|
||||
t.Errorf("echoed stream data mismatch: got %s, want %s", mb[0].Bytes(), streamData)
|
||||
}
|
||||
buf.ReleaseMulti(mb)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if receivedDest.NetAddr() != dest.NetAddr() {
|
||||
t.Errorf("destination mismatch: got %s, want %s", receivedDest.NetAddr(), dest.NetAddr())
|
||||
}
|
||||
if diff := cmp.Diff(receivedPayload, testPayload); diff != "" {
|
||||
t.Errorf("payload mismatch: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPCodec(t *testing.T) {
|
||||
methods := []string{
|
||||
MethodAES128GCM,
|
||||
MethodAES256GCM,
|
||||
MethodChaCha20Poly1305,
|
||||
}
|
||||
|
||||
dest := net.UDPDestination(net.LocalHostIP, net.Port(53))
|
||||
payload := []byte("DNS query payload")
|
||||
|
||||
for _, methodName := range methods {
|
||||
t.Run(methodName, func(t *testing.T) {
|
||||
method, err := GetCipherMethod(methodName)
|
||||
common.Must(err)
|
||||
|
||||
psk := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(psk)
|
||||
|
||||
clientCodec, err := NewUDPPacketCodec(method, psk)
|
||||
common.Must(err)
|
||||
serverCodec, err := NewUDPServerCodec(method, psk, time.Minute)
|
||||
common.Must(err)
|
||||
|
||||
pktBuf, err := clientCodec.EncodeClientPacket(dest, payload)
|
||||
common.Must(err)
|
||||
defer pktBuf.Release()
|
||||
|
||||
decoded, err := serverCodec.DecodePacket(pktBuf.Bytes())
|
||||
common.Must(err)
|
||||
|
||||
if decoded.HeaderType != HeaderTypeClient {
|
||||
t.Errorf("expected header type %d, got %d", HeaderTypeClient, decoded.HeaderType)
|
||||
}
|
||||
if decoded.Destination.Port != dest.Port {
|
||||
t.Errorf("port mismatch: got %d, want %d", decoded.Destination.Port, dest.Port)
|
||||
}
|
||||
if !bytes.Equal(decoded.Payload, payload) {
|
||||
t.Errorf("payload mismatch: got %s, want %s", decoded.Payload, payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiUserManager(t *testing.T) {
|
||||
masterKey := generateRandomKey(16)
|
||||
userKey1 := generateRandomKey(16)
|
||||
userKey2 := generateRandomKey(16)
|
||||
|
||||
config := &MultiUserServerConfig{
|
||||
Method: MethodAES128GCM,
|
||||
Key: masterKey,
|
||||
Users: []*protocol.User{
|
||||
{
|
||||
Email: "user1@example.com",
|
||||
Account: serial.ToTypedMessage(&Account{Key: userKey1}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
inbound, err := NewMultiServer(newTestContext(), config)
|
||||
common.Must(err)
|
||||
|
||||
if inbound.GetUsersCount(context.Background()) != 1 {
|
||||
t.Fatalf("expected 1 user, got %d", inbound.GetUsersCount(context.Background()))
|
||||
}
|
||||
|
||||
u1 := inbound.GetUser(context.Background(), "user1@example.com")
|
||||
if u1 == nil || u1.Email != "user1@example.com" {
|
||||
t.Fatal("user1 not found")
|
||||
}
|
||||
|
||||
// Add User 2
|
||||
rawKey2, _ := base64.StdEncoding.DecodeString(userKey2)
|
||||
u2 := &protocol.MemoryUser{
|
||||
Email: "user2@example.com",
|
||||
Account: &MemoryAccount{
|
||||
Key: rawKey2,
|
||||
},
|
||||
}
|
||||
err = inbound.AddUser(context.Background(), u2)
|
||||
common.Must(err)
|
||||
|
||||
if inbound.GetUsersCount(context.Background()) != 2 {
|
||||
t.Fatalf("expected 2 users, got %d", inbound.GetUsersCount(context.Background()))
|
||||
}
|
||||
|
||||
// Remove User 1
|
||||
err = inbound.RemoveUser(context.Background(), "user1@example.com")
|
||||
common.Must(err)
|
||||
|
||||
if inbound.GetUsersCount(context.Background()) != 1 {
|
||||
t.Fatalf("expected 1 user, got %d", inbound.GetUsersCount(context.Background()))
|
||||
}
|
||||
if inbound.GetUser(context.Background(), "user1@example.com") != nil {
|
||||
t.Fatal("user1 should have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
type dummyDispatcher struct {
|
||||
onDispatch func(ctx context.Context, dest net.Destination) (*transport.Link, error)
|
||||
}
|
||||
|
||||
func (d *dummyDispatcher) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) {
|
||||
if d.onDispatch != nil {
|
||||
return d.onDispatch(ctx, dest)
|
||||
}
|
||||
return nil, errors.New("not handled")
|
||||
}
|
||||
|
||||
func (d *dummyDispatcher) DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dummyDispatcher) Start() error { return nil }
|
||||
func (d *dummyDispatcher) Close() error { return nil }
|
||||
func (d *dummyDispatcher) Type() interface{} { return routing.DispatcherType() }
|
||||
|
||||
type dummyStatConn struct {
|
||||
gonet.Conn
|
||||
}
|
||||
|
||||
func (c *dummyStatConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
b := buf.New()
|
||||
_, err := b.ReadFrom(c.Conn)
|
||||
return buf.MultiBuffer{b}, err
|
||||
}
|
||||
|
||||
func (c *dummyStatConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
defer buf.ReleaseMulti(mb)
|
||||
for _, b := range mb {
|
||||
if _, err := c.Conn.Write(b.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMultiUserTCPConnection(t *testing.T) {
|
||||
masterKey := generateRandomKey(16)
|
||||
userKey1 := generateRandomKey(16)
|
||||
userKey2 := generateRandomKey(16)
|
||||
|
||||
config := &MultiUserServerConfig{
|
||||
Method: MethodAES128GCM,
|
||||
Key: masterKey,
|
||||
Users: []*protocol.User{
|
||||
{
|
||||
Email: "user1@example.com",
|
||||
Account: serial.ToTypedMessage(&Account{Key: userKey1}),
|
||||
},
|
||||
{
|
||||
Email: "user2@example.com",
|
||||
Account: serial.ToTypedMessage(&Account{Key: userKey2}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCtx := newTestContext()
|
||||
inbound, err := NewMultiServer(testCtx, config)
|
||||
common.Must(err)
|
||||
|
||||
clientConn, serverConn := gonet.Pipe()
|
||||
defer clientConn.Close()
|
||||
defer serverConn.Close()
|
||||
|
||||
dest := net.TCPDestination(net.LocalHostIP, 443)
|
||||
method, err := GetCipherMethod(MethodAES128GCM)
|
||||
common.Must(err)
|
||||
|
||||
masterRaw, _ := base64.StdEncoding.DecodeString(masterKey)
|
||||
user2Raw, _ := base64.StdEncoding.DecodeString(userKey2)
|
||||
clientPSKList := [][]byte{masterRaw, user2Raw}
|
||||
|
||||
dispatchedUserChan := make(chan string, 1)
|
||||
|
||||
disp := &dummyDispatcher{
|
||||
onDispatch: func(ctx context.Context, d net.Destination) (*transport.Link, error) {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
if inbound != nil && inbound.User != nil {
|
||||
dispatchedUserChan <- inbound.User.Email
|
||||
}
|
||||
link := &transport.Link{
|
||||
Reader: buf.NewReader(bytes.NewReader(nil)),
|
||||
Writer: buf.Discard,
|
||||
}
|
||||
return link, nil
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = inbound.Process(testCtx, net.Network_TCP, &dummyStatConn{Conn: serverConn}, disp)
|
||||
}()
|
||||
|
||||
clientSalt, writer, err := ClientHandshake(clientConn, method, clientPSKList, dest, []byte("ping"))
|
||||
common.Must(err)
|
||||
|
||||
reader, _, err := ClientVerifyServerResponse(clientConn, method, user2Raw, clientSalt)
|
||||
common.Must(err)
|
||||
_ = writer
|
||||
_ = reader
|
||||
|
||||
select {
|
||||
case email := <-dispatchedUserChan:
|
||||
if email != "user2@example.com" {
|
||||
t.Fatalf("expected user2@example.com, got %s", email)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timeout waiting for dispatched user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPReaderWriter(t *testing.T) {
|
||||
for _, methodName := range []string{MethodAES128GCM, MethodAES256GCM, MethodChaCha20Poly1305} {
|
||||
t.Run(methodName, func(t *testing.T) {
|
||||
method, err := GetCipherMethod(methodName)
|
||||
common.Must(err)
|
||||
rawPSK := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(rawPSK)
|
||||
|
||||
clientCodec, err := NewUDPPacketCodec(method, rawPSK)
|
||||
common.Must(err)
|
||||
serverCodec, err := NewUDPServerCodec(method, rawPSK, time.Minute)
|
||||
common.Must(err)
|
||||
|
||||
dest := net.UDPDestination(net.LocalHostIP, 53)
|
||||
|
||||
// Client to Server
|
||||
clientPacketBuf, err := clientCodec.EncodeClientPacket(dest, []byte("hello dns"))
|
||||
common.Must(err)
|
||||
defer clientPacketBuf.Release()
|
||||
|
||||
serverDecoded, err := serverCodec.DecodePacket(clientPacketBuf.Bytes())
|
||||
common.Must(err)
|
||||
if string(serverDecoded.Payload) != "hello dns" {
|
||||
t.Fatalf("unexpected server decoded payload: %s", string(serverDecoded.Payload))
|
||||
}
|
||||
|
||||
// Server to Client
|
||||
serverPacket, err := serverCodec.EncodePacket(serverDecoded.SessionID, dest, []byte("dns response"))
|
||||
common.Must(err)
|
||||
|
||||
clientDecoded, err := clientCodec.DecodePacket(serverPacket)
|
||||
common.Must(err)
|
||||
if string(clientDecoded.Payload) != "dns response" {
|
||||
t.Fatalf("unexpected client decoded payload: %s", string(clientDecoded.Payload))
|
||||
}
|
||||
|
||||
// Test UDPWriter and UDPReader pipeline
|
||||
pipeR, pipeW := gonet.Pipe()
|
||||
defer pipeR.Close()
|
||||
defer pipeW.Close()
|
||||
|
||||
writer := &UDPWriter{
|
||||
Writer: pipeW,
|
||||
Destination: dest,
|
||||
Codec: clientCodec,
|
||||
}
|
||||
reader := &UDPReader{
|
||||
Reader: pipeR,
|
||||
Codec: clientCodec,
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Simulate server echoing back as server response
|
||||
buf := make([]byte, 2048)
|
||||
n, err := pipeR.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dec, err := serverCodec.DecodePacket(buf[:n])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := serverCodec.EncodePacket(dec.SessionID, dest, dec.Payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = pipeW.Write(resp)
|
||||
}()
|
||||
|
||||
b := buf.New()
|
||||
b.WriteString("piped udp packet")
|
||||
common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b}))
|
||||
|
||||
received, err := reader.ReadMultiBuffer()
|
||||
common.Must(err)
|
||||
if received[0].String() != "piped udp packet" {
|
||||
t.Fatalf("expected 'piped udp packet', got '%s'", received[0].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPRequestResponse(t *testing.T) {
|
||||
for _, methodName := range []string{MethodAES128GCM, MethodAES256GCM, MethodChaCha20Poly1305} {
|
||||
t.Run(methodName, func(t *testing.T) {
|
||||
method, err := GetCipherMethod(methodName)
|
||||
common.Must(err)
|
||||
rawPSK := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(rawPSK)
|
||||
|
||||
clientConn, serverConn := gonet.Pipe()
|
||||
defer clientConn.Close()
|
||||
defer serverConn.Close()
|
||||
|
||||
clientSalt := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(clientSalt)
|
||||
dest := net.TCPDestination(net.LocalHostIP, 80)
|
||||
|
||||
go func() {
|
||||
// Server side: read handshake and verify clientSalt
|
||||
salt := make([]byte, method.KeySaltLength)
|
||||
if _, err := io.ReadFull(serverConn, salt); err != nil {
|
||||
t.Errorf("server read salt error: %v", err)
|
||||
return
|
||||
}
|
||||
sessionKey := DeriveSessionSubKey(rawPSK, salt, method.KeySaltLength)
|
||||
aead, err := method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
t.Errorf("server AEAD error: %v", err)
|
||||
return
|
||||
}
|
||||
sReader := NewStreamReader(serverConn, aead)
|
||||
var fixedBuf [RequestHeaderFixedChunkLength + AEADTagSize]byte
|
||||
if _, err := io.ReadFull(serverConn, fixedBuf[:]); err != nil {
|
||||
t.Errorf("server read fixed error: %v", err)
|
||||
return
|
||||
}
|
||||
plainFixed, err := aead.Open(fixedBuf[:0], sReader.Nonce(), fixedBuf[:], nil)
|
||||
if err != nil {
|
||||
t.Errorf("server decrypt fixed error: %v", err)
|
||||
return
|
||||
}
|
||||
IncreaseNonce(sReader.Nonce())
|
||||
|
||||
varLen := int(binary.BigEndian.Uint16(plainFixed[9:11]))
|
||||
varBuf := make([]byte, varLen+AEADTagSize)
|
||||
if _, err := io.ReadFull(serverConn, varBuf); err != nil {
|
||||
t.Errorf("server read var error: %v", err)
|
||||
return
|
||||
}
|
||||
plainVar, err := aead.Open(varBuf[:0], sReader.Nonce(), varBuf, nil)
|
||||
if err != nil {
|
||||
t.Errorf("server decrypt var error: %v", err)
|
||||
return
|
||||
}
|
||||
IncreaseNonce(sReader.Nonce())
|
||||
|
||||
vBuf := buf.New()
|
||||
vBuf.Write(plainVar)
|
||||
receivedDest, err := ReadAddressPort(vBuf)
|
||||
if err != nil || receivedDest != dest {
|
||||
t.Errorf("dest mismatch: %v vs %v, err: %v", receivedDest, dest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Echo client salt back to client using WriteTCPResponse
|
||||
sWriter, err := WriteTCPResponse(serverConn, method, rawPSK, salt, []byte("early-reply"))
|
||||
if err != nil {
|
||||
t.Errorf("server response error: %v", err)
|
||||
return
|
||||
}
|
||||
_ = sWriter
|
||||
}()
|
||||
|
||||
bodyWriter, err := WriteTCPRequest(clientConn, method, [][]byte{rawPSK}, dest, clientSalt, nil)
|
||||
common.Must(err)
|
||||
_ = bodyWriter
|
||||
|
||||
responseReader, err := ReadTCPResponse(clientConn, method, rawPSK, clientSalt)
|
||||
common.Must(err)
|
||||
|
||||
mb, err := responseReader.ReadMultiBuffer()
|
||||
common.Must(err)
|
||||
if mb[0].String() != "early-reply" {
|
||||
t.Fatalf("expected early-reply, got %s", mb[0].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPReplayProtection(t *testing.T) {
|
||||
for _, methodName := range []string{MethodAES128GCM, MethodAES256GCM, MethodChaCha20Poly1305} {
|
||||
t.Run(methodName, func(t *testing.T) {
|
||||
method, err := GetCipherMethod(methodName)
|
||||
common.Must(err)
|
||||
rawPSK := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(rawPSK)
|
||||
|
||||
clientCodec, err := NewUDPPacketCodec(method, rawPSK)
|
||||
common.Must(err)
|
||||
serverCodec, err := NewUDPServerCodec(method, rawPSK, time.Minute)
|
||||
common.Must(err)
|
||||
|
||||
dest := net.UDPDestination(net.LocalHostIP, 53)
|
||||
pktBuf, err := clientCodec.EncodeClientPacket(dest, []byte("dns 1"))
|
||||
common.Must(err)
|
||||
defer pktBuf.Release()
|
||||
|
||||
rawCopy := make([]byte, pktBuf.Len())
|
||||
copy(rawCopy, pktBuf.Bytes())
|
||||
|
||||
// First decode should succeed
|
||||
_, err = serverCodec.DecodePacket(pktBuf.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("first decode failed: %v", err)
|
||||
}
|
||||
|
||||
// Replay same packet wire bytes should fail with ErrPacketIdNotUnique
|
||||
_, err = serverCodec.DecodePacket(rawCopy)
|
||||
if err != ErrPacketIdNotUnique {
|
||||
t.Fatalf("expected ErrPacketIdNotUnique on replay, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerUDPSessionStabilityAndMonotonicPacketID(t *testing.T) {
|
||||
for _, methodName := range []string{MethodAES128GCM, MethodAES256GCM, MethodChaCha20Poly1305} {
|
||||
t.Run(methodName, func(t *testing.T) {
|
||||
method, err := GetCipherMethod(methodName)
|
||||
common.Must(err)
|
||||
rawPSK := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(rawPSK)
|
||||
|
||||
clientCodec, err := NewUDPPacketCodec(method, rawPSK)
|
||||
common.Must(err)
|
||||
serverCodec, err := NewUDPServerCodec(method, rawPSK, time.Minute)
|
||||
common.Must(err)
|
||||
|
||||
dest := net.UDPDestination(net.LocalHostIP, 53)
|
||||
|
||||
// Client sends packet 1
|
||||
pkt1, err := clientCodec.EncodeClientPacket(dest, []byte("request 1"))
|
||||
common.Must(err)
|
||||
defer pkt1.Release()
|
||||
|
||||
dec1, err := serverCodec.DecodePacket(pkt1.Bytes())
|
||||
common.Must(err)
|
||||
|
||||
// Server sends response 1
|
||||
resp1, err := serverCodec.EncodePacket(dec1.SessionID, dest, []byte("response 1"))
|
||||
common.Must(err)
|
||||
|
||||
// Server sends response 2 to the same client session
|
||||
resp2, err := serverCodec.EncodePacket(dec1.SessionID, dest, []byte("response 2"))
|
||||
common.Must(err)
|
||||
|
||||
// Decode both on client
|
||||
cDec1, err := clientCodec.DecodePacket(resp1)
|
||||
common.Must(err)
|
||||
cDec2, err := clientCodec.DecodePacket(resp2)
|
||||
common.Must(err)
|
||||
|
||||
if cDec1.SessionID != cDec2.SessionID {
|
||||
t.Fatalf("expected stable server session ID, got %d and %d", cDec1.SessionID, cDec2.SessionID)
|
||||
}
|
||||
if cDec2.PacketID <= cDec1.PacketID {
|
||||
t.Fatalf("expected monotonically increasing packet ID, got %d then %d", cDec1.PacketID, cDec2.PacketID)
|
||||
}
|
||||
if string(cDec1.Payload) != "response 1" || string(cDec2.Payload) != "response 2" {
|
||||
t.Fatalf("payload mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockDialer struct {
|
||||
dial func(ctx context.Context, dest net.Destination) (stat.Connection, error)
|
||||
}
|
||||
|
||||
func (d *mockDialer) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
|
||||
if d.dial != nil {
|
||||
return d.dial(ctx, dest)
|
||||
}
|
||||
c1, c2 := gonet.Pipe()
|
||||
_ = c2.Close()
|
||||
return &dummyStatConn{Conn: c1}, nil
|
||||
}
|
||||
|
||||
func (d *mockDialer) DestIpAddress() net.IP {
|
||||
return net.IP{127, 0, 0, 1}
|
||||
}
|
||||
|
||||
func (d *mockDialer) SetOutboundGateway(ctx context.Context, ob *session.Outbound) {}
|
||||
|
||||
func TestOutboundProcess(t *testing.T) {
|
||||
testCtx := newTestContext()
|
||||
key := generateRandomKey(16)
|
||||
clientConfig := &ClientConfig{
|
||||
Address: &net.IPOrDomain{Address: &net.IPOrDomain_Ip{Ip: []byte{127, 0, 0, 1}}},
|
||||
Port: 1080,
|
||||
Method: MethodAES128GCM,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
outbound, err := NewClient(testCtx, clientConfig)
|
||||
common.Must(err)
|
||||
|
||||
ctx := session.ContextWithOutbounds(testCtx, []*session.Outbound{
|
||||
{
|
||||
Target: net.TCPDestination(net.LocalHostIP, 80),
|
||||
},
|
||||
})
|
||||
|
||||
link := &transport.Link{
|
||||
Reader: buf.NewReader(bytes.NewReader(nil)),
|
||||
Writer: buf.Discard,
|
||||
}
|
||||
|
||||
dialer := &mockDialer{}
|
||||
err = outbound.Process(ctx, link, dialer)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from closed mock dialer pipe, got nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
mrand "math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
)
|
||||
|
||||
var addrParser = protocol.NewAddressParser(
|
||||
protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4),
|
||||
protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6),
|
||||
protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain),
|
||||
protocol.WithAddressTypeParser(func(b byte) byte {
|
||||
return b & 0x0F
|
||||
}),
|
||||
)
|
||||
|
||||
func IncreaseNonce(nonce []byte) {
|
||||
for i := range nonce {
|
||||
nonce[i]++
|
||||
if nonce[i] != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAddressPort writes a destination address and port in SOCKS5 format
|
||||
func WriteAddressPort(w io.Writer, dest net.Destination) error {
|
||||
return addrParser.WriteAddressPort(w, dest.Address, dest.Port)
|
||||
}
|
||||
|
||||
// ReadAddressPort reads a destination address and port in SOCKS5 format
|
||||
func ReadAddressPort(r io.Reader) (net.Destination, error) {
|
||||
addr, port, err := addrParser.ReadAddressPort(nil, r)
|
||||
if err != nil {
|
||||
return net.Destination{}, err
|
||||
}
|
||||
return net.TCPDestination(addr, port), nil
|
||||
}
|
||||
|
||||
// AddrPortLength returns the serialized length of a destination in SOCKS5 format
|
||||
func AddrPortLength(dest net.Destination) int {
|
||||
switch dest.Address.Family() {
|
||||
case net.AddressFamilyIPv4:
|
||||
return 1 + 4 + 2
|
||||
case net.AddressFamilyDomain:
|
||||
return 1 + 1 + len(dest.Address.Domain()) + 2
|
||||
case net.AddressFamilyIPv6:
|
||||
return 1 + 16 + 2
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type StreamWriter struct {
|
||||
writer io.Writer
|
||||
cipher cipher.AEAD
|
||||
nonce [StreamNonceSize]byte
|
||||
lenBuf [2]byte
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func NewStreamWriter(w io.Writer, c cipher.AEAD) *StreamWriter {
|
||||
return &StreamWriter{
|
||||
writer: w,
|
||||
cipher: c,
|
||||
buf: make([]byte, 0, MaxPacketSize+2+2*AEADTagSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *StreamWriter) Nonce() []byte {
|
||||
return w.nonce[:]
|
||||
}
|
||||
|
||||
func (w *StreamWriter) Cipher() cipher.AEAD {
|
||||
return w.cipher
|
||||
}
|
||||
|
||||
func (w *StreamWriter) WriteChunk(payload []byte) error {
|
||||
payloadLen := len(payload)
|
||||
if payloadLen == 0 {
|
||||
return nil
|
||||
}
|
||||
if payloadLen > MaxPacketSize {
|
||||
return errors.New("payload exceeds MaxPacketSize")
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(w.lenBuf[:], uint16(payloadLen))
|
||||
w.buf = w.cipher.Seal(w.buf[:0], w.nonce[:], w.lenBuf[:], nil)
|
||||
IncreaseNonce(w.nonce[:])
|
||||
|
||||
w.buf = w.cipher.Seal(w.buf, w.nonce[:], payload, nil)
|
||||
IncreaseNonce(w.nonce[:])
|
||||
|
||||
_, err := w.writer.Write(w.buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *StreamWriter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
for len(p) > 0 {
|
||||
chunkSize := len(p)
|
||||
if chunkSize > MaxPacketSize {
|
||||
chunkSize = MaxPacketSize
|
||||
}
|
||||
if err := w.WriteChunk(p[:chunkSize]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
p = p[chunkSize:]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *StreamWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
defer buf.ReleaseMulti(mb)
|
||||
for _, b := range mb {
|
||||
if err := w.WriteChunk(b.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StreamReader struct {
|
||||
reader io.Reader
|
||||
cipher cipher.AEAD
|
||||
nonce [StreamNonceSize]byte
|
||||
lenBuf [2 + AEADTagSize]byte
|
||||
buffer []byte
|
||||
cached int
|
||||
offset int
|
||||
}
|
||||
|
||||
func NewStreamReader(r io.Reader, c cipher.AEAD) *StreamReader {
|
||||
return &StreamReader{
|
||||
reader: r,
|
||||
cipher: c,
|
||||
buffer: make([]byte, MaxPacketSize+AEADTagSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *StreamReader) Nonce() []byte {
|
||||
return r.nonce[:]
|
||||
}
|
||||
|
||||
func (r *StreamReader) Cipher() cipher.AEAD {
|
||||
return r.cipher
|
||||
}
|
||||
|
||||
func (r *StreamReader) Read(p []byte) (int, error) {
|
||||
if r.cached > 0 {
|
||||
n := copy(p, r.buffer[r.offset:r.offset+r.cached])
|
||||
r.cached -= n
|
||||
r.offset += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Read 2-byte length + AEAD tag (18 bytes)
|
||||
if _, err := io.ReadFull(r.reader, r.lenBuf[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
decryptedLen, err := r.cipher.Open(r.lenBuf[:0], r.nonce[:], r.lenBuf[:], nil)
|
||||
if err != nil {
|
||||
return 0, errors.New("failed to decrypt chunk length").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
payloadLen := int(binary.BigEndian.Uint16(decryptedLen))
|
||||
if payloadLen == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
chunkEnd := payloadLen + AEADTagSize
|
||||
if _, err := io.ReadFull(r.reader, r.buffer[:chunkEnd]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
decryptedPayload, err := r.cipher.Open(r.buffer[:0], r.nonce[:], r.buffer[:chunkEnd], nil)
|
||||
if err != nil {
|
||||
return 0, errors.New("failed to decrypt chunk payload").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
r.cached = len(decryptedPayload)
|
||||
r.offset = 0
|
||||
|
||||
n := copy(p, r.buffer[r.offset:r.offset+r.cached])
|
||||
r.cached -= n
|
||||
r.offset += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *StreamReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
if r.cached > 0 {
|
||||
b := buf.New()
|
||||
b.Write(r.buffer[r.offset : r.offset+r.cached])
|
||||
r.cached = 0
|
||||
r.offset = 0
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(r.reader, r.lenBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedLen, err := r.cipher.Open(r.lenBuf[:0], r.nonce[:], r.lenBuf[:], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt chunk length").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
payloadLen := int(binary.BigEndian.Uint16(decryptedLen))
|
||||
if payloadLen == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
chunkEnd := payloadLen + AEADTagSize
|
||||
if _, err := io.ReadFull(r.reader, r.buffer[:chunkEnd]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedPayload, err := r.cipher.Open(r.buffer[:0], r.nonce[:], r.buffer[:chunkEnd], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt chunk payload").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
b := buf.New()
|
||||
b.Write(decryptedPayload)
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
|
||||
type ClientRequestHeader struct {
|
||||
Destination net.Destination
|
||||
EarlyData []byte
|
||||
}
|
||||
|
||||
func ReadClientRequestHeader(conn io.Reader, reader *StreamReader) (*ClientRequestHeader, error) {
|
||||
var fixedBuf [RequestHeaderFixedChunkLength + AEADTagSize]byte
|
||||
if _, err := io.ReadFull(conn, fixedBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainFixed, err := reader.cipher.Open(fixedBuf[:0], reader.Nonce(), fixedBuf[:], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt client request header").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.Nonce())
|
||||
|
||||
if plainFixed[0] != HeaderTypeClient {
|
||||
return nil, ErrBadHeaderType
|
||||
}
|
||||
|
||||
epoch := binary.BigEndian.Uint64(plainFixed[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
|
||||
varHeaderLen := int(binary.BigEndian.Uint16(plainFixed[9:11]))
|
||||
if varHeaderLen == 0 {
|
||||
return nil, ErrInvalidRequest
|
||||
}
|
||||
|
||||
var stackVarChunk [512]byte
|
||||
var varChunkCipher []byte
|
||||
needed := varHeaderLen + AEADTagSize
|
||||
if needed <= len(stackVarChunk) {
|
||||
varChunkCipher = stackVarChunk[:needed]
|
||||
} else {
|
||||
varChunkCipher = make([]byte, needed)
|
||||
}
|
||||
if _, err := io.ReadFull(conn, varChunkCipher); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainVar, err := reader.cipher.Open(varChunkCipher[:0], reader.Nonce(), varChunkCipher, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt variable request header").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.Nonce())
|
||||
|
||||
b := buf.New()
|
||||
b.Write(plainVar)
|
||||
defer b.Release()
|
||||
|
||||
dest, err := ReadAddressPort(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var padLenBytes [2]byte
|
||||
if _, err := b.Read(padLenBytes[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paddingLen := int(binary.BigEndian.Uint16(padLenBytes[:]))
|
||||
if int(b.Len()) < paddingLen {
|
||||
return nil, ErrNoPadding
|
||||
}
|
||||
if paddingLen > 0 {
|
||||
b.Advance(int32(paddingLen))
|
||||
}
|
||||
|
||||
var earlyData []byte
|
||||
if b.Len() > 0 {
|
||||
earlyData = make([]byte, b.Len())
|
||||
copy(earlyData, b.Bytes())
|
||||
}
|
||||
|
||||
return &ClientRequestHeader{
|
||||
Destination: dest,
|
||||
EarlyData: earlyData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClientHandshake writes the full client request header to w
|
||||
func ClientHandshake(w io.Writer, method *CipherMethod, pskList [][]byte, dest net.Destination, payload []byte) ([]byte, *StreamWriter, error) {
|
||||
salt := make([]byte, method.KeySaltLength)
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
writer, err := WriteTCPRequest(w, method, pskList, dest, salt, payload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return salt, writer.(*StreamWriter), nil
|
||||
}
|
||||
|
||||
// ClientVerifyServerResponse reads and verifies the server's handshake response
|
||||
func ClientVerifyServerResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt []byte) (*StreamReader, []byte, error) {
|
||||
reader, err := ReadTCPResponse(r, method, psk, clientSalt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sr := reader.(*StreamReader)
|
||||
var initialPayload []byte
|
||||
if sr.cached > 0 {
|
||||
initialPayload = make([]byte, sr.cached)
|
||||
copy(initialPayload, sr.buffer[sr.offset:sr.offset+sr.cached])
|
||||
}
|
||||
return sr, initialPayload, nil
|
||||
}
|
||||
|
||||
// WriteTCPRequest writes the Shadowsocks 2022 request header into w and returns a body writer.
|
||||
func WriteTCPRequest(w io.Writer, method *CipherMethod, pskList [][]byte, dest net.Destination, clientSalt []byte, payload []byte) (buf.Writer, error) {
|
||||
finalPSK := pskList[len(pskList)-1]
|
||||
sessionKey := DeriveSessionSubKey(finalPSK, clientSalt, method.KeySaltLength)
|
||||
aead, err := method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writer := NewStreamWriter(w, aead)
|
||||
|
||||
handshakeBuf := buf.New()
|
||||
defer handshakeBuf.Release()
|
||||
|
||||
handshakeBuf.Write(clientSalt)
|
||||
|
||||
if len(pskList) > 1 {
|
||||
for i := 0; i < len(pskList)-1; i++ {
|
||||
currPSK := pskList[i]
|
||||
identitySubkey := DeriveIdentitySubKey(currPSK, clientSalt, method.KeySaltLength)
|
||||
block, err := method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nextPSK := pskList[i+1]
|
||||
pskHash := DeriveUserPSKHash(nextPSK)
|
||||
var encryptedEIH [AESBlockSize]byte
|
||||
block.Encrypt(encryptedEIH[:], pskHash[:])
|
||||
handshakeBuf.Write(encryptedEIH[:])
|
||||
}
|
||||
}
|
||||
|
||||
payloadLen := len(payload)
|
||||
var paddingLen int
|
||||
if payloadLen < MaxPaddingLength {
|
||||
paddingLen = mrand.Intn(MaxPaddingLength-payloadLen) + 1
|
||||
}
|
||||
addrPortLen := AddrPortLength(dest)
|
||||
varHeaderLen := addrPortLen + 2 + paddingLen + payloadLen
|
||||
|
||||
var fixedHeaderPlaintext [RequestHeaderFixedChunkLength]byte
|
||||
fixedHeaderPlaintext[0] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(fixedHeaderPlaintext[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(fixedHeaderPlaintext[9:11], uint16(varHeaderLen))
|
||||
|
||||
fixedChunk := writer.cipher.Seal(nil, writer.nonce[:], fixedHeaderPlaintext[:], nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
handshakeBuf.Write(fixedChunk)
|
||||
|
||||
varHeaderBuf := buf.New()
|
||||
defer varHeaderBuf.Release()
|
||||
|
||||
if err := WriteAddressPort(varHeaderBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var padLenBytes [2]byte
|
||||
binary.BigEndian.PutUint16(padLenBytes[:], uint16(paddingLen))
|
||||
varHeaderBuf.Write(padLenBytes[:])
|
||||
|
||||
if paddingLen > 0 {
|
||||
varHeaderBuf.Write(zeroPadding[:paddingLen])
|
||||
}
|
||||
|
||||
if payloadLen > 0 {
|
||||
varHeaderBuf.Write(payload)
|
||||
}
|
||||
|
||||
varChunk := writer.cipher.Seal(nil, writer.nonce[:], varHeaderBuf.Bytes(), nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
handshakeBuf.Write(varChunk)
|
||||
|
||||
if _, err := w.Write(handshakeBuf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// ReadTCPResponse reads and verifies the server's handshake response and returns a reader for the stream.
|
||||
func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt []byte) (buf.Reader, error) {
|
||||
var serverSalt [32]byte
|
||||
serverSaltSlice := serverSalt[:method.KeySaltLength]
|
||||
if _, err := io.ReadFull(r, serverSaltSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionKey := DeriveSessionSubKey(psk, serverSaltSlice, method.KeySaltLength)
|
||||
aead, err := method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := NewStreamReader(r, aead)
|
||||
|
||||
fixedPlainLen := 1 + 8 + method.KeySaltLength + 2
|
||||
chunkCipherLen := fixedPlainLen + AEADTagSize
|
||||
var chunkBuf [64]byte
|
||||
chunkSlice := chunkBuf[:chunkCipherLen]
|
||||
if _, err := io.ReadFull(r, chunkSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedFixed, err := reader.cipher.Open(chunkSlice[:0], reader.nonce[:], chunkSlice, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt server response header").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.nonce[:])
|
||||
|
||||
if decryptedFixed[0] != HeaderTypeServer {
|
||||
return nil, ErrBadHeaderType
|
||||
}
|
||||
|
||||
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch))))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
|
||||
echoedSalt := decryptedFixed[9 : 9+method.KeySaltLength]
|
||||
for i := 0; i < method.KeySaltLength; i++ {
|
||||
if echoedSalt[i] != clientSalt[i] {
|
||||
return nil, errors.New("bad request salt")
|
||||
}
|
||||
}
|
||||
|
||||
initialPayloadLen := int(binary.BigEndian.Uint16(decryptedFixed[9+method.KeySaltLength : 11+method.KeySaltLength]))
|
||||
if initialPayloadLen > 0 {
|
||||
initialCipherLen := initialPayloadLen + AEADTagSize
|
||||
if _, err := io.ReadFull(r, reader.buffer[:initialCipherLen]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decryptedInitial, err := reader.cipher.Open(reader.buffer[:0], reader.nonce[:], reader.buffer[:initialCipherLen], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt initial response payload").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.nonce[:])
|
||||
reader.cached = len(decryptedInitial)
|
||||
reader.offset = 0
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// WriteTCPResponse writes the server handshake response and returns a body writer for server stream.
|
||||
func WriteTCPResponse(w io.Writer, method *CipherMethod, psk []byte, clientSalt []byte, initialPayload []byte) (buf.Writer, error) {
|
||||
var serverSalt [32]byte
|
||||
serverSaltSlice := serverSalt[:method.KeySaltLength]
|
||||
if _, err := io.ReadFull(rand.Reader, serverSaltSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respKey := DeriveSessionSubKey(psk, serverSaltSlice, method.KeySaltLength)
|
||||
respAead, err := method.NewAEAD(respKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer := NewStreamWriter(w, respAead)
|
||||
|
||||
respBuf := buf.New()
|
||||
defer respBuf.Release()
|
||||
|
||||
respBuf.Write(serverSaltSlice)
|
||||
|
||||
var fixedRespPlain [1 + 8 + 32 + 2]byte
|
||||
fixedRespSlice := fixedRespPlain[:1+8+method.KeySaltLength+2]
|
||||
fixedRespSlice[0] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(fixedRespSlice[1:9], uint64(time.Now().Unix()))
|
||||
copy(fixedRespSlice[9:9+method.KeySaltLength], clientSalt)
|
||||
binary.BigEndian.PutUint16(fixedRespSlice[9+method.KeySaltLength:11+method.KeySaltLength], uint16(len(initialPayload)))
|
||||
|
||||
fixedRespChunk := writer.cipher.Seal(nil, writer.nonce[:], fixedRespSlice, nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
respBuf.Write(fixedRespChunk)
|
||||
|
||||
if len(initialPayload) > 0 {
|
||||
initialChunk := writer.cipher.Seal(nil, writer.nonce[:], initialPayload, nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
respBuf.Write(initialChunk)
|
||||
}
|
||||
|
||||
if _, err := w.Write(respBuf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
+4
-33
@@ -37,25 +37,6 @@ type Handler struct {
|
||||
downlinkCounter stats.Counter
|
||||
}
|
||||
|
||||
type tunUDPStatsWriter struct {
|
||||
writer buf.Writer
|
||||
counter stats.Counter
|
||||
}
|
||||
|
||||
func (w *tunUDPStatsWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
for len(mb) > 0 {
|
||||
remaining, packet := buf.SplitFirst(mb)
|
||||
packetSize := packet.Len()
|
||||
if err := w.writer.WriteMultiBuffer(buf.MultiBuffer{packet}); err != nil {
|
||||
buf.ReleaseMulti(remaining)
|
||||
return err
|
||||
}
|
||||
w.counter.Add(int64(packetSize))
|
||||
mb = remaining
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectionHandler interface with the only method that stack is going to push new connections to
|
||||
type ConnectionHandler interface {
|
||||
HandleConnection(conn net.Conn, destination net.Destination)
|
||||
@@ -123,7 +104,7 @@ func (t *Handler) Start() error {
|
||||
iface := updater.Get()
|
||||
if iface == nil {
|
||||
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
|
||||
return errors.New("iface not found")
|
||||
return nil
|
||||
}
|
||||
return c.Control(func(fd uintptr) {
|
||||
addrPort, _ := netip.ParseAddrPort(address)
|
||||
@@ -190,8 +171,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
|
||||
return
|
||||
}
|
||||
source := net.DestinationFromAddr(remote)
|
||||
isUDP := destination.Network == net.Network_UDP
|
||||
if !isUDP && (t.uplinkCounter != nil || t.downlinkCounter != nil) {
|
||||
if t.uplinkCounter != nil || t.downlinkCounter != nil {
|
||||
conn = &stat.CounterConnection{
|
||||
Connection: conn,
|
||||
ReadCounter: t.uplinkCounter,
|
||||
@@ -223,18 +203,9 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
|
||||
})
|
||||
errors.LogInfo(ctx, "processing from ", source, " to ", destination)
|
||||
|
||||
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}
|
||||
writer := buf.NewWriter(conn)
|
||||
if isUDP {
|
||||
reader.Counter = t.uplinkCounter
|
||||
if t.downlinkCounter != nil {
|
||||
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
|
||||
}
|
||||
}
|
||||
|
||||
link := &transport.Link{
|
||||
Reader: reader,
|
||||
Writer: writer,
|
||||
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
|
||||
Writer: buf.NewWriter(conn),
|
||||
}
|
||||
if err := t.dispatcher.DispatchLink(ctx, destination, link); err != nil {
|
||||
errors.LogError(ctx, errors.New("connection closed").Base(err))
|
||||
|
||||
+140
-125
@@ -3,6 +3,7 @@ package wireguard
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -27,10 +28,14 @@ import (
|
||||
"github.com/xtls/xray-core/features/stats"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
got []net.IP
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
conf *DeviceConfig
|
||||
policyManager policy.Manager
|
||||
@@ -44,6 +49,11 @@ type Handler struct {
|
||||
tnet *Net
|
||||
dev *device.Device
|
||||
mu sync.Mutex
|
||||
|
||||
// TODO: cache cleanup loop
|
||||
local bool
|
||||
cache map[string]entry
|
||||
cacheMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
@@ -99,10 +109,15 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
local := false
|
||||
dns := conf.DNS
|
||||
if len(dns) == 0 {
|
||||
dns = []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"}
|
||||
}
|
||||
if len(dns) == 1 && dns[0] == "local" {
|
||||
local = true
|
||||
dns = nil
|
||||
}
|
||||
dnses := make([]netip.Addr, 0, len(dns))
|
||||
for _, dns := range dns {
|
||||
dnses = append(dnses, netip.MustParseAddr(dns))
|
||||
@@ -136,6 +151,9 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
|
||||
tun: tun,
|
||||
tnet: tnet,
|
||||
|
||||
local: local,
|
||||
cache: make(map[string]entry),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -154,6 +172,22 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return err
|
||||
}
|
||||
|
||||
var addr netip.Addr
|
||||
if ob.Target.Address.Family().IsDomain() {
|
||||
ip, err := h.resolveRemote(ob.Target.Address.String())
|
||||
if err != nil {
|
||||
return errors.New("failed to resolve domain").Base(err)
|
||||
}
|
||||
addr, _ = netip.AddrFromSlice(ip)
|
||||
} else {
|
||||
addr, _ = netip.AddrFromSlice(ob.Target.Address.IP())
|
||||
}
|
||||
|
||||
addrPort := netip.AddrPortFrom(addr, ob.Target.Port.Value())
|
||||
if !addrPort.IsValid() {
|
||||
return errors.New("invalid target ", ob.Target)
|
||||
}
|
||||
|
||||
var newCtx context.Context
|
||||
var newCancel context.CancelFunc
|
||||
if session.TimeoutOnlyFromContext(ctx) {
|
||||
@@ -182,10 +216,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
var err error
|
||||
if sessionPolicy.Timeouts.Handshake != 0 {
|
||||
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, sessionPolicy.Timeouts.Handshake)
|
||||
conn, err = h.tnet.DialContext(timeoutCtx, "tcp", ob.Target.NetAddr())
|
||||
conn, err = h.tnet.DialContextTCPAddrPort(timeoutCtx, addrPort)
|
||||
timeoutCancel()
|
||||
} else {
|
||||
conn, err = h.tnet.Dial("tcp", ob.Target.NetAddr())
|
||||
conn, err = h.tnet.DialContextTCPAddrPort(ctx, addrPort)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("failed to create TCP connection").Base(err)
|
||||
@@ -194,14 +228,15 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
reader = buf.NewReader(conn)
|
||||
writer = buf.NewWriter(conn)
|
||||
case net.Network_UDP:
|
||||
conn, err := h.tnet.Dial("udp", ob.Target.NetAddr())
|
||||
conn, err := h.tnet.DialUDPAddrPort(netip.AddrPort{}, addrPort)
|
||||
if err != nil {
|
||||
return errors.New("failed to create UDP connection").Base(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
c := &UDPConnClient{
|
||||
PacketConn: conn.(*internet.PacketConnWrapper).PacketConn,
|
||||
Dest: conn.RemoteAddr().(*net.UDPAddr),
|
||||
c := &udpConnClient{
|
||||
PacketConn: conn.(*internet.PacketConnWrapper).PacketConn,
|
||||
resolveFunc: h.resolveRemote,
|
||||
dest: gonet.UDPAddrFromAddrPort(addrPort),
|
||||
}
|
||||
reader = c
|
||||
writer = c
|
||||
@@ -258,26 +293,26 @@ func (h *Handler) init(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pktConn net.PacketConn
|
||||
if h.streamSettings.FinalMask != nil {
|
||||
conn, err := h.streamSettings.FinalMask.DialUDP(ctx, dest)
|
||||
switch c := conn.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
if h.streamSettings.UdpmaskManager != nil {
|
||||
newConn, err := h.streamSettings.UdpmaskManager.WrapPacketConnClient(pktConn)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
pktConn = conn.(*finalmask.PacketConnWrapper).PacketConn
|
||||
} else {
|
||||
conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
switch c := conn.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
pktConn.Close()
|
||||
return nil, errors.New("mask err").Base(err)
|
||||
}
|
||||
pktConn = newConn
|
||||
}
|
||||
if h.uplinkCounter != nil || h.downlinkCounter != nil {
|
||||
pktConn = &PacketCounterConnection{
|
||||
@@ -336,54 +371,90 @@ func (h *Handler) init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
||||
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) resolveRemote(host string) (net.IP, error) {
|
||||
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||
if h.local {
|
||||
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
}
|
||||
return h.tnet.LookupHost(host)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, uint32, error)) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
h.cacheMu.Lock()
|
||||
if entry, ok := h.cache[host]; ok {
|
||||
if time.Now().Before(entry.time) {
|
||||
h.cacheMu.Unlock()
|
||||
return entry.got[dice.Roll(len(entry.got))], nil
|
||||
}
|
||||
delete(h.cache, host)
|
||||
}
|
||||
h.cacheMu.Unlock()
|
||||
ips, ttl, err := lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
got := ips
|
||||
if h.streamSettings.SocketSettings != nil {
|
||||
var got4, got6 []net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
got4 = append(got4, ip)
|
||||
} else {
|
||||
got6 = append(got6, ip)
|
||||
}
|
||||
}
|
||||
switch h.streamSettings.SocketSettings.DomainStrategy {
|
||||
case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP:
|
||||
got = ips
|
||||
case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4:
|
||||
got = got4
|
||||
case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6:
|
||||
got = got6
|
||||
case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46:
|
||||
got = got4
|
||||
if len(got) == 0 {
|
||||
got = got6
|
||||
}
|
||||
case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64:
|
||||
got = got6
|
||||
if len(got) == 0 {
|
||||
got = got4
|
||||
}
|
||||
}
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
if len(ips) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
}
|
||||
var got4, got6 []net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
got4 = append(got4, ip)
|
||||
} else {
|
||||
got6 = append(got6, ip)
|
||||
}
|
||||
}
|
||||
var got []net.IP
|
||||
switch strategy {
|
||||
case DeviceConfig_FORCE_IP:
|
||||
got = ips
|
||||
return ips[dice.Roll(len(ips))], nil
|
||||
case DeviceConfig_FORCE_IP4:
|
||||
got = got4
|
||||
case DeviceConfig_FORCE_IP6:
|
||||
got = got6
|
||||
case DeviceConfig_FORCE_IP46:
|
||||
got = got4
|
||||
if len(got) == 0 {
|
||||
got = got6
|
||||
}
|
||||
case DeviceConfig_FORCE_IP64:
|
||||
got = got6
|
||||
if len(got) == 0 {
|
||||
got = got4
|
||||
}
|
||||
default:
|
||||
panic(strategy)
|
||||
}
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
}
|
||||
entry := entry{
|
||||
got: got,
|
||||
time: time.Now().Add(time.Duration(ttl) * time.Second),
|
||||
}
|
||||
h.cacheMu.Lock()
|
||||
h.cache[host] = entry
|
||||
h.cacheMu.Unlock()
|
||||
return got[dice.Roll(len(got))], nil
|
||||
}
|
||||
|
||||
type UDPConnClient struct {
|
||||
type udpConnClient struct {
|
||||
net.PacketConn
|
||||
Dest *net.UDPAddr
|
||||
resolveFunc func(host string) (net.IP, error)
|
||||
dest *net.UDPAddr
|
||||
}
|
||||
|
||||
func (c *UDPConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
func (c *udpConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
b := buf.New()
|
||||
b.Resize(0, buf.Size)
|
||||
n, addr, err := c.PacketConn.ReadFrom(b.Bytes())
|
||||
@@ -402,13 +473,20 @@ func (c *UDPConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
|
||||
func (c *UDPConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
for i, b := range mb {
|
||||
dst := c.Dest
|
||||
dst := c.dest
|
||||
if b.UDP != nil {
|
||||
if b.UDP.Address.Family().IsDomain() {
|
||||
if b.UDP.Port != net.Port(dst.Port) {
|
||||
dst = &net.UDPAddr{IP: dst.IP, Port: int(b.UDP.Port)}
|
||||
ip, err := c.resolveFunc(b.UDP.Address.String())
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "drop packet to ", b.UDP, " with size ", len(b.Bytes()))
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
dst = &net.UDPAddr{
|
||||
IP: ip,
|
||||
Port: int(b.UDP.Port),
|
||||
}
|
||||
} else {
|
||||
dst = b.UDP.RawNetAddr().(*net.UDPAddr)
|
||||
@@ -445,66 +523,3 @@ func (c *PacketCounterConnection) WriteTo(p []byte, addr net.Addr) (n int, err e
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
saddr []string
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
running bool
|
||||
m map[string]entry
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *cache) run() {
|
||||
if c.running {
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
if c.m == nil {
|
||||
c.m = make(map[string]entry)
|
||||
}
|
||||
go c.gc()
|
||||
}
|
||||
|
||||
func (c *cache) gc() {
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
for now := range ticker.C {
|
||||
c.mu.Lock()
|
||||
for key, entry := range c.m {
|
||||
if now.After(entry.deadline) {
|
||||
delete(c.m, key)
|
||||
}
|
||||
}
|
||||
if len(c.m) == 0 {
|
||||
c.running = false
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) LookupHost(host string) []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.run()
|
||||
if entry, ok := c.m[host]; ok {
|
||||
if time.Now().Before(entry.deadline) {
|
||||
return entry.saddr
|
||||
}
|
||||
delete(c.m, host)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cache) Cache(host string, saddr []string, ttl uint32) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.m[host] = entry{
|
||||
saddr: saddr,
|
||||
deadline: time.Now().Add(time.Second * time.Duration(ttl)),
|
||||
}
|
||||
}
|
||||
|
||||
+102
-26
@@ -22,6 +22,61 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type DeviceConfig_DomainStrategy int32
|
||||
|
||||
const (
|
||||
DeviceConfig_FORCE_IP DeviceConfig_DomainStrategy = 0
|
||||
DeviceConfig_FORCE_IP4 DeviceConfig_DomainStrategy = 1
|
||||
DeviceConfig_FORCE_IP6 DeviceConfig_DomainStrategy = 2
|
||||
DeviceConfig_FORCE_IP46 DeviceConfig_DomainStrategy = 3
|
||||
DeviceConfig_FORCE_IP64 DeviceConfig_DomainStrategy = 4
|
||||
)
|
||||
|
||||
// Enum value maps for DeviceConfig_DomainStrategy.
|
||||
var (
|
||||
DeviceConfig_DomainStrategy_name = map[int32]string{
|
||||
0: "FORCE_IP",
|
||||
1: "FORCE_IP4",
|
||||
2: "FORCE_IP6",
|
||||
3: "FORCE_IP46",
|
||||
4: "FORCE_IP64",
|
||||
}
|
||||
DeviceConfig_DomainStrategy_value = map[string]int32{
|
||||
"FORCE_IP": 0,
|
||||
"FORCE_IP4": 1,
|
||||
"FORCE_IP6": 2,
|
||||
"FORCE_IP46": 3,
|
||||
"FORCE_IP64": 4,
|
||||
}
|
||||
)
|
||||
|
||||
func (x DeviceConfig_DomainStrategy) Enum() *DeviceConfig_DomainStrategy {
|
||||
p := new(DeviceConfig_DomainStrategy)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x DeviceConfig_DomainStrategy) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (DeviceConfig_DomainStrategy) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_proxy_wireguard_config_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (DeviceConfig_DomainStrategy) Type() protoreflect.EnumType {
|
||||
return &file_proxy_wireguard_config_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x DeviceConfig_DomainStrategy) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeviceConfig_DomainStrategy.Descriptor instead.
|
||||
func (DeviceConfig_DomainStrategy) EnumDescriptor() ([]byte, []int) {
|
||||
return file_proxy_wireguard_config_proto_rawDescGZIP(), []int{1, 0}
|
||||
}
|
||||
|
||||
type PeerConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PublicKey string `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||
@@ -99,18 +154,19 @@ func (x *PeerConfig) GetAllowedIps() []string {
|
||||
}
|
||||
|
||||
type DeviceConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
|
||||
Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"`
|
||||
Users []*protocol.User `protobuf:"bytes,5,rep,name=users,proto3" json:"users,omitempty"`
|
||||
Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"`
|
||||
Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"`
|
||||
IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"`
|
||||
NoKernelTun bool `protobuf:"varint,9,opt,name=no_kernel_tun,json=noKernelTun,proto3" json:"no_kernel_tun,omitempty"`
|
||||
DNS []string `protobuf:"bytes,10,rep,name=DNS,proto3" json:"DNS,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
|
||||
Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"`
|
||||
Users []*protocol.User `protobuf:"bytes,5,rep,name=users,proto3" json:"users,omitempty"`
|
||||
Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"`
|
||||
Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"`
|
||||
DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"`
|
||||
IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"`
|
||||
NoKernelTun bool `protobuf:"varint,9,opt,name=no_kernel_tun,json=noKernelTun,proto3" json:"no_kernel_tun,omitempty"`
|
||||
DNS []string `protobuf:"bytes,10,rep,name=DNS,proto3" json:"DNS,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeviceConfig) Reset() {
|
||||
@@ -185,6 +241,13 @@ func (x *DeviceConfig) GetReserved() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *DeviceConfig) GetDomainStrategy() DeviceConfig_DomainStrategy {
|
||||
if x != nil {
|
||||
return x.DomainStrategy
|
||||
}
|
||||
return DeviceConfig_FORCE_IP
|
||||
}
|
||||
|
||||
func (x *DeviceConfig) GetIsClient() bool {
|
||||
if x != nil {
|
||||
return x.IsClient
|
||||
@@ -220,7 +283,7 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
|
||||
"\vallowed_ips\x18\x05 \x03(\tR\n" +
|
||||
"allowedIps\"\xb4\x02\n" +
|
||||
"allowedIps\"\xee\x03\n" +
|
||||
"\fDeviceConfig\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" +
|
||||
@@ -228,11 +291,20 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
"\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x120\n" +
|
||||
"\x05users\x18\x05 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x12\x10\n" +
|
||||
"\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1a\n" +
|
||||
"\breserved\x18\x06 \x01(\fR\breserved\x12\x1b\n" +
|
||||
"\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" +
|
||||
"\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" +
|
||||
"\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" +
|
||||
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\x12\x10\n" +
|
||||
"\x03DNS\x18\n" +
|
||||
" \x03(\tR\x03DNSB^\n" +
|
||||
" \x03(\tR\x03DNS\"\\\n" +
|
||||
"\x0eDomainStrategy\x12\f\n" +
|
||||
"\bFORCE_IP\x10\x00\x12\r\n" +
|
||||
"\tFORCE_IP4\x10\x01\x12\r\n" +
|
||||
"\tFORCE_IP6\x10\x02\x12\x0e\n" +
|
||||
"\n" +
|
||||
"FORCE_IP46\x10\x03\x12\x0e\n" +
|
||||
"\n" +
|
||||
"FORCE_IP64\x10\x04B^\n" +
|
||||
"\x18com.xray.proxy.wireguardP\x01Z)github.com/xtls/xray-core/proxy/wireguard\xaa\x02\x14Xray.Proxy.WireGuardb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -247,20 +319,23 @@ func file_proxy_wireguard_config_proto_rawDescGZIP() []byte {
|
||||
return file_proxy_wireguard_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proxy_wireguard_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_proxy_wireguard_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_proxy_wireguard_config_proto_goTypes = []any{
|
||||
(*PeerConfig)(nil), // 0: xray.proxy.wireguard.PeerConfig
|
||||
(*DeviceConfig)(nil), // 1: xray.proxy.wireguard.DeviceConfig
|
||||
(*protocol.User)(nil), // 2: xray.common.protocol.User
|
||||
(DeviceConfig_DomainStrategy)(0), // 0: xray.proxy.wireguard.DeviceConfig.DomainStrategy
|
||||
(*PeerConfig)(nil), // 1: xray.proxy.wireguard.PeerConfig
|
||||
(*DeviceConfig)(nil), // 2: xray.proxy.wireguard.DeviceConfig
|
||||
(*protocol.User)(nil), // 3: xray.common.protocol.User
|
||||
}
|
||||
var file_proxy_wireguard_config_proto_depIdxs = []int32{
|
||||
0, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig
|
||||
2, // 1: xray.proxy.wireguard.DeviceConfig.users:type_name -> xray.common.protocol.User
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
1, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig
|
||||
3, // 1: xray.proxy.wireguard.DeviceConfig.users:type_name -> xray.common.protocol.User
|
||||
0, // 2: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proxy_wireguard_config_proto_init() }
|
||||
@@ -273,13 +348,14 @@ func file_proxy_wireguard_config_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_wireguard_config_proto_rawDesc), len(file_proxy_wireguard_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumEnums: 1,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_proxy_wireguard_config_proto_goTypes,
|
||||
DependencyIndexes: file_proxy_wireguard_config_proto_depIdxs,
|
||||
EnumInfos: file_proxy_wireguard_config_proto_enumTypes,
|
||||
MessageInfos: file_proxy_wireguard_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proxy_wireguard_config_proto = out.File
|
||||
|
||||
@@ -17,6 +17,13 @@ message PeerConfig {
|
||||
}
|
||||
|
||||
message DeviceConfig {
|
||||
enum DomainStrategy {
|
||||
FORCE_IP = 0;
|
||||
FORCE_IP4 = 1;
|
||||
FORCE_IP6 = 2;
|
||||
FORCE_IP46 = 3;
|
||||
FORCE_IP64 = 4;
|
||||
}
|
||||
string secret_key = 1;
|
||||
repeated string endpoint = 2;
|
||||
repeated PeerConfig peers = 3;
|
||||
@@ -24,6 +31,7 @@ message DeviceConfig {
|
||||
int32 mtu = 4;
|
||||
|
||||
bytes reserved = 6;
|
||||
DomainStrategy domain_strategy = 7;
|
||||
bool is_client = 8;
|
||||
bool no_kernel_tun = 9;
|
||||
repeated string DNS = 10;
|
||||
|
||||
+14
-159
@@ -15,8 +15,6 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -44,7 +42,6 @@ type netTun struct {
|
||||
events chan tun.Event
|
||||
notifyHandle *channel.NotificationHandle
|
||||
incomingPacket chan *buffer.View
|
||||
closed chan struct{}
|
||||
mtu int
|
||||
dnsServers []netip.Addr
|
||||
hasV4, hasV6 bool
|
||||
@@ -61,7 +58,6 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int, handleLocal
|
||||
stack: stack.New(opts),
|
||||
events: make(chan tun.Event, 10),
|
||||
incomingPacket: make(chan *buffer.View),
|
||||
closed: make(chan struct{}),
|
||||
dnsServers: dnsServers,
|
||||
mtu: mtu,
|
||||
}
|
||||
@@ -128,15 +124,12 @@ func (tun *netTun) Events() <-chan tun.Event {
|
||||
}
|
||||
|
||||
func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
|
||||
var view *buffer.View
|
||||
select {
|
||||
case view = <-tun.incomingPacket:
|
||||
case <-tun.closed:
|
||||
view, ok := <-tun.incomingPacket
|
||||
if !ok {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
|
||||
n, err := view.Read(buf[0][offset:])
|
||||
view.Release()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -173,11 +166,7 @@ func (tun *netTun) WriteNotify() {
|
||||
view := pkt.ToView()
|
||||
pkt.DecRef()
|
||||
|
||||
select {
|
||||
case tun.incomingPacket <- view:
|
||||
case <-tun.closed:
|
||||
view.Release()
|
||||
}
|
||||
tun.incomingPacket <- view
|
||||
}
|
||||
|
||||
func (tun *netTun) Close() error {
|
||||
@@ -190,9 +179,8 @@ func (tun *netTun) Close() error {
|
||||
close(tun.events)
|
||||
}
|
||||
|
||||
// we don't close incomingPacket, because WriteNotify may be mid-send on it (DNS lookup) and would panic.
|
||||
if tun.closed != nil {
|
||||
close(tun.closed)
|
||||
if tun.incomingPacket != nil {
|
||||
close(tun.incomingPacket)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -231,7 +219,6 @@ type Net struct {
|
||||
DialUDPAddrPort func(laddr, raddr netip.AddrPort) (net.Conn, error)
|
||||
dnsServers []netip.Addr
|
||||
hasV4, hasV6 bool
|
||||
cache cache
|
||||
}
|
||||
|
||||
func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) {
|
||||
@@ -259,12 +246,9 @@ var (
|
||||
errServerTemporarilyMisbehaving = errors.New("server misbehaving")
|
||||
errCanceled = errors.New("operation was canceled")
|
||||
errTimeout = errors.New("i/o timeout")
|
||||
errNumericPort = errors.New("port must be numeric")
|
||||
errNoSuitableAddress = errors.New("no suitable address found")
|
||||
errMissingAddress = errors.New("missing address")
|
||||
)
|
||||
|
||||
func (net *Net) LookupHost(host string) (addrs []string, err error) {
|
||||
func (net *Net) LookupHost(host string) (addrs []net.IP, ttl uint32, err error) {
|
||||
return net.LookupContextHost(context.Background(), host)
|
||||
}
|
||||
|
||||
@@ -583,12 +567,9 @@ func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.T
|
||||
return dnsmessage.Parser{}, "", lastErr
|
||||
}
|
||||
|
||||
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) {
|
||||
if saddr := tnet.cache.LookupHost(host); saddr != nil {
|
||||
return saddr, nil
|
||||
}
|
||||
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, uint32, error) {
|
||||
if host == "" || (!tnet.hasV6 && !tnet.hasV4) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
}
|
||||
zlen := len(host)
|
||||
if strings.IndexByte(host, ':') != -1 {
|
||||
@@ -597,11 +578,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host[:zlen]); err == nil {
|
||||
return []string{ip.String()}, nil
|
||||
return []net.IP{ip.AsSlice()}, 0, nil
|
||||
}
|
||||
|
||||
if !isDomainName(host) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
}
|
||||
type result struct {
|
||||
p dnsmessage.Parser
|
||||
@@ -702,137 +683,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
|
||||
if len(addrs) == 0 && lastErr != nil {
|
||||
return nil, lastErr
|
||||
return nil, 0, lastErr
|
||||
}
|
||||
saddrs := make([]string, 0, len(addrs))
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, ip := range addrs {
|
||||
saddrs = append(saddrs, ip.String())
|
||||
ips = append(ips, ip.AsSlice())
|
||||
}
|
||||
tnet.cache.Cache(host, saddrs, ttl)
|
||||
return saddrs, nil
|
||||
}
|
||||
|
||||
func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) {
|
||||
if deadline.IsZero() {
|
||||
return deadline, nil
|
||||
}
|
||||
timeRemaining := deadline.Sub(now)
|
||||
if timeRemaining <= 0 {
|
||||
return time.Time{}, errTimeout
|
||||
}
|
||||
timeout := timeRemaining / time.Duration(addrsRemaining)
|
||||
const saneMinimum = 2 * time.Second
|
||||
if timeout < saneMinimum {
|
||||
if timeRemaining < saneMinimum {
|
||||
timeout = timeRemaining
|
||||
} else {
|
||||
timeout = saneMinimum
|
||||
}
|
||||
}
|
||||
return now.Add(timeout), nil
|
||||
}
|
||||
|
||||
var protoSplitter = regexp.MustCompile(`^(tcp|udp|ping)(4|6)?$`)
|
||||
|
||||
func (tnet *Net) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
var acceptV4, acceptV6 bool
|
||||
matches := protoSplitter.FindStringSubmatch(network)
|
||||
if matches == nil {
|
||||
return nil, &net.OpError{Op: "dial", Err: net.UnknownNetworkError(network)}
|
||||
} else if len(matches[2]) == 0 {
|
||||
acceptV4 = true
|
||||
acceptV6 = true
|
||||
} else {
|
||||
acceptV4 = matches[2][0] == '4'
|
||||
acceptV6 = !acceptV4
|
||||
}
|
||||
var host string
|
||||
var port int
|
||||
if matches[1] == "ping" {
|
||||
host = address
|
||||
} else {
|
||||
var sport string
|
||||
var err error
|
||||
host, sport, err = net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "dial", Err: err}
|
||||
}
|
||||
port, err = strconv.Atoi(sport)
|
||||
if err != nil || port < 0 || port > 65535 {
|
||||
return nil, &net.OpError{Op: "dial", Err: errNumericPort}
|
||||
}
|
||||
}
|
||||
allAddr, err := tnet.LookupContextHost(ctx, host)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "dial", Err: err}
|
||||
}
|
||||
var addrs []netip.AddrPort
|
||||
for _, addr := range allAddr {
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err == nil && ((ip.Is4() && acceptV4) || (ip.Is6() && acceptV6)) {
|
||||
addrs = append(addrs, netip.AddrPortFrom(ip, uint16(port)))
|
||||
}
|
||||
}
|
||||
if len(addrs) == 0 && len(allAddr) != 0 {
|
||||
return nil, &net.OpError{Op: "dial", Err: errNoSuitableAddress}
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for i, addr := range addrs {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := ctx.Err()
|
||||
if err == context.Canceled {
|
||||
err = errCanceled
|
||||
} else if err == context.DeadlineExceeded {
|
||||
err = errTimeout
|
||||
}
|
||||
return nil, &net.OpError{Op: "dial", Err: err}
|
||||
default:
|
||||
}
|
||||
|
||||
dialCtx := ctx
|
||||
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
|
||||
partialDeadline, err := partialDeadline(time.Now(), deadline, len(addrs)-i)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = &net.OpError{Op: "dial", Err: err}
|
||||
}
|
||||
break
|
||||
}
|
||||
if partialDeadline.Before(deadline) {
|
||||
var cancel context.CancelFunc
|
||||
dialCtx, cancel = context.WithDeadline(ctx, partialDeadline)
|
||||
defer cancel()
|
||||
}
|
||||
}
|
||||
|
||||
var c net.Conn
|
||||
switch matches[1] {
|
||||
case "tcp":
|
||||
c, err = tnet.DialContextTCPAddrPort(dialCtx, addr)
|
||||
case "udp":
|
||||
c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, addr)
|
||||
case "ping":
|
||||
err = errors.New("not support")
|
||||
// c, err = tnet.DialPingAddr(netip.Addr{}, addr.Addr())
|
||||
}
|
||||
if err == nil {
|
||||
return c, nil
|
||||
}
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
if firstErr == nil {
|
||||
firstErr = &net.OpError{Op: "dial", Err: errMissingAddress}
|
||||
}
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
func (tnet *Net) Dial(network, address string) (net.Conn, error) {
|
||||
return tnet.DialContext(context.Background(), network, address)
|
||||
return ips, ttl, nil
|
||||
}
|
||||
|
||||
@@ -258,16 +258,18 @@ func (s *Server) Start() error {
|
||||
return errors.New("address is domain")
|
||||
}
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
var pktConn net.PacketConn
|
||||
var err error
|
||||
if s.streamSettings.FinalMask != nil {
|
||||
pktConn, err = s.streamSettings.FinalMask.ListenPacket(context.Background(), &net.UDPAddr{IP: s.src.Address.IP(), Port: int(s.src.Port)})
|
||||
} else {
|
||||
pktConn, err = internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: s.src.Address.IP(), Port: int(s.src.Port)}, s.streamSettings.SocketSettings)
|
||||
}
|
||||
pktConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: s.src.Address.IP(), Port: int(s.src.Port)}, s.streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.streamSettings.UdpmaskManager != nil {
|
||||
newConn, err := s.streamSettings.UdpmaskManager.WrapPacketConnServer(pktConn)
|
||||
if err != nil {
|
||||
pktConn.Close()
|
||||
return nil, errors.New("mask err").Base(err)
|
||||
}
|
||||
pktConn = newConn
|
||||
}
|
||||
if s.uplinkCounter != nil || s.downlinkCounter != nil {
|
||||
pktConn = &PacketCounterConnection{
|
||||
PacketConn: pktConn,
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
package scenarios
|
||||
|
||||
import (
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
"crypto/x509"
|
||||
go_errors "errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||||
|
||||
"github.com/xtls/xray-core/app/log"
|
||||
"github.com/xtls/xray-core/app/proxyman"
|
||||
"github.com/xtls/xray-core/common"
|
||||
clog "github.com/xtls/xray-core/common/log"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/protocol/tls/cert"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
core "github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/dokodemo"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/proxy/wireguard"
|
||||
"github.com/xtls/xray-core/testing/servers/tcp"
|
||||
"github.com/xtls/xray-core/testing/servers/udp"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
transmasque "github.com/xtls/xray-core/transport/internet/masque"
|
||||
"github.com/xtls/xray-core/transport/internet/masque/connectip"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
var (
|
||||
masqueServerV4 = netip.MustParseAddr("10.13.0.1")
|
||||
masqueServerV6 = netip.MustParseAddr("fd13::1")
|
||||
masqueClientV4 = netip.MustParsePrefix("10.13.0.2/32")
|
||||
masqueClientV6 = netip.MustParsePrefix("fd13::2/128")
|
||||
)
|
||||
|
||||
const (
|
||||
masqueEchoPort = 7
|
||||
masqueAuthorization = "Basic dTpw"
|
||||
)
|
||||
|
||||
func startMasqueServer(t *testing.T) (net.Port, [32]byte) {
|
||||
dev, _, gstack, err := wireguard.CreateNetTUN([]netip.Addr{masqueServerV4, masqueServerV6}, nil, transmasque.MinPacketSize, false)
|
||||
common.Must(err)
|
||||
t.Cleanup(func() { dev.Close() })
|
||||
|
||||
for _, addr := range []netip.Addr{masqueServerV4, masqueServerV6} {
|
||||
proto := ipv4.ProtocolNumber
|
||||
if addr.Is6() {
|
||||
proto = ipv6.ProtocolNumber
|
||||
}
|
||||
local := tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(addr.AsSlice()), Port: masqueEchoPort}
|
||||
l, err := gonet.ListenTCP(gstack, local, proto)
|
||||
common.Must(err)
|
||||
go func() {
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer c.Close()
|
||||
b := make([]byte, 2048)
|
||||
for {
|
||||
n, err := c.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := c.Write(xor(b[:n])); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
u, err := gonet.DialUDP(gstack, &local, nil, proto)
|
||||
common.Must(err)
|
||||
go func() {
|
||||
b := make([]byte, 2048)
|
||||
for {
|
||||
n, addr, err := u.ReadFrom(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
u.WriteTo(xor(b[:n]), addr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var current atomic.Pointer[connectip.Conn]
|
||||
go func() {
|
||||
bufs := [][]byte{make([]byte, transmasque.MinPacketSize)}
|
||||
sizes := []int{0}
|
||||
for {
|
||||
if _, err := dev.Read(bufs, sizes, 0); err != nil {
|
||||
return
|
||||
}
|
||||
if conn := current.Load(); conn != nil {
|
||||
if icmp, _ := conn.WritePacket(bufs[0][:sizes[0]]); len(icmp) > 0 {
|
||||
go dev.Write([][]byte{icmp}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != transmasque.DefaultPath {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Header.Get("Authorization") != masqueAuthorization {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
req, err := connectip.ParseProxyRequest(r)
|
||||
if err != nil {
|
||||
var perr *connectip.ProxyRequestParseError
|
||||
if go_errors.As(err, &perr) {
|
||||
w.WriteHeader(perr.HTTPStatus)
|
||||
}
|
||||
return
|
||||
}
|
||||
conn, err := (&connectip.Proxy{}).Proxy(w, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
common.Must(conn.AssignAddresses([]netip.Prefix{masqueClientV4, masqueClientV6}))
|
||||
common.Must(conn.AdvertiseRoute([]connectip.IPRoute{
|
||||
{StartIP: netip.IPv4Unspecified(), EndIP: netip.AddrFrom4([4]byte{255, 255, 255, 255})},
|
||||
{StartIP: netip.IPv6Unspecified(), EndIP: netip.AddrFrom16([16]byte{0: 0xff, 1: 0xff, 2: 0xff, 3: 0xff, 4: 0xff, 5: 0xff, 6: 0xff, 7: 0xff, 8: 0xff, 9: 0xff, 10: 0xff, 11: 0xff, 12: 0xff, 13: 0xff, 14: 0xff, 15: 0xff})},
|
||||
}))
|
||||
go func() {
|
||||
for {
|
||||
ar, err := conn.ReceiveAddressRequest(context.Background())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
assigned := make([]netip.Prefix, len(ar.Prefixes))
|
||||
for i, p := range ar.Prefixes {
|
||||
if p.Addr().Is4() {
|
||||
assigned[i] = masqueClientV4
|
||||
} else {
|
||||
assigned[i] = masqueClientV6
|
||||
}
|
||||
}
|
||||
ar.Respond(assigned, nil)
|
||||
}
|
||||
}()
|
||||
current.Store(conn)
|
||||
b := make([]byte, 2048)
|
||||
for {
|
||||
n, err := conn.ReadPacket(b)
|
||||
if err != nil {
|
||||
if go_errors.Is(err, io.ErrShortBuffer) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
dev.Write([][]byte{b[:n]}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
certificate, certHash := cert.MustGenerate(nil, cert.CommonName("localhost"))
|
||||
key := common.Must2(x509.ParsePKCS8PrivateKey(certificate.PrivateKey))
|
||||
tlsConfig := &gotls.Config{
|
||||
Certificates: []gotls.Certificate{{Certificate: [][]byte{certificate.Certificate}, PrivateKey: key}},
|
||||
NextProtos: []string{http3.NextProtoH3},
|
||||
}
|
||||
pktConn := common.Must2(net.ListenUDP("udp", &net.UDPAddr{IP: net.LocalHostIP.IP()}))
|
||||
tr := &quic.Transport{Conn: pktConn}
|
||||
ln := common.Must2(tr.ListenEarly(tlsConfig, &quic.Config{EnableDatagrams: true, InitialPacketSize: 1350}))
|
||||
server := &http3.Server{Handler: http.HandlerFunc(handler), EnableDatagrams: true}
|
||||
go server.ServeListener(ln)
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
ln.Close()
|
||||
tr.Close()
|
||||
pktConn.Close()
|
||||
})
|
||||
|
||||
return net.Port(pktConn.LocalAddr().(*net.UDPAddr).Port), certHash
|
||||
}
|
||||
|
||||
func TestMasque(t *testing.T) {
|
||||
serverPort, certHash := startMasqueServer(t)
|
||||
|
||||
tcpPort := tcp.PickPort()
|
||||
tcp6Port := tcp.PickPort()
|
||||
udpPort := udp.PickPort()
|
||||
dokodemoTo := func(port net.Port, addr netip.Addr, network net.Network) *core.InboundHandlerConfig {
|
||||
return &core.InboundHandlerConfig{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(port)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
RewriteAddress: net.NewIPOrDomain(net.IPAddress(addr.AsSlice())),
|
||||
RewritePort: masqueEchoPort,
|
||||
AllowedNetworks: []net.Network{network},
|
||||
}),
|
||||
}
|
||||
}
|
||||
clientConfig := &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogLevel: clog.Severity_Debug,
|
||||
ErrorLogType: log.LogType_Console,
|
||||
}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
dokodemoTo(tcpPort, masqueServerV4, net.Network_TCP),
|
||||
dokodemoTo(tcp6Port, masqueServerV6, net.Network_TCP),
|
||||
dokodemoTo(udpPort, masqueServerV4, net.Network_UDP),
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&masque.ClientConfig{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(serverPort),
|
||||
},
|
||||
}),
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "masque",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "masque",
|
||||
Settings: serial.ToTypedMessage(&transmasque.Config{
|
||||
Path: transmasque.DefaultPath,
|
||||
Headers: map[string]string{"Authorization": masqueAuthorization},
|
||||
}),
|
||||
},
|
||||
},
|
||||
SecurityType: serial.GetMessageType(&tls.Config{}),
|
||||
SecuritySettings: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&tls.Config{
|
||||
ServerName: "localhost",
|
||||
PinnedPeerCertSha256: [][]byte{certHash[:]},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
servers, err := InitializeServerConfigs(clientConfig)
|
||||
common.Must(err)
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
var errg errgroup.Group
|
||||
for range 3 {
|
||||
errg.Go(testTCPConn(tcpPort, 1024*1024, time.Second*20))
|
||||
}
|
||||
errg.Go(testTCPConn(tcp6Port, 1024*1024, time.Second*20))
|
||||
errg.Go(testUDPConn(udpPort, 1024, time.Second*5))
|
||||
if err := errg.Wait(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
"github.com/xtls/xray-core/app/log"
|
||||
"github.com/xtls/xray-core/app/proxyman"
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -22,9 +21,19 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var ss2022Methods = []string{
|
||||
shadowsocks_2022.MethodAES128GCM,
|
||||
shadowsocks_2022.MethodAES256GCM,
|
||||
shadowsocks_2022.MethodChaCha20Poly1305,
|
||||
}
|
||||
|
||||
func TestShadowsocks2022Tcp(t *testing.T) {
|
||||
for _, method := range shadowaead_2022.List {
|
||||
password := make([]byte, 32)
|
||||
for _, method := range ss2022Methods {
|
||||
keySize := 32
|
||||
if method == shadowsocks_2022.MethodAES128GCM {
|
||||
keySize = 16
|
||||
}
|
||||
password := make([]byte, keySize)
|
||||
rand.Read(password)
|
||||
t.Run(method, func(t *testing.T) {
|
||||
testShadowsocks2022Tcp(t, method, base64.StdEncoding.EncodeToString(password))
|
||||
@@ -33,21 +42,21 @@ func TestShadowsocks2022Tcp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShadowsocks2022UdpAES128(t *testing.T) {
|
||||
password := make([]byte, 32)
|
||||
password := make([]byte, 16)
|
||||
rand.Read(password)
|
||||
testShadowsocks2022Udp(t, shadowaead_2022.List[0], base64.StdEncoding.EncodeToString(password))
|
||||
testShadowsocks2022Udp(t, shadowsocks_2022.MethodAES128GCM, base64.StdEncoding.EncodeToString(password))
|
||||
}
|
||||
|
||||
func TestShadowsocks2022UdpAES256(t *testing.T) {
|
||||
password := make([]byte, 32)
|
||||
rand.Read(password)
|
||||
testShadowsocks2022Udp(t, shadowaead_2022.List[1], base64.StdEncoding.EncodeToString(password))
|
||||
testShadowsocks2022Udp(t, shadowsocks_2022.MethodAES256GCM, base64.StdEncoding.EncodeToString(password))
|
||||
}
|
||||
|
||||
func TestShadowsocks2022UdpChacha(t *testing.T) {
|
||||
password := make([]byte, 32)
|
||||
rand.Read(password)
|
||||
testShadowsocks2022Udp(t, shadowaead_2022.List[2], base64.StdEncoding.EncodeToString(password))
|
||||
testShadowsocks2022Udp(t, shadowsocks_2022.MethodChaCha20Poly1305, base64.StdEncoding.EncodeToString(password))
|
||||
}
|
||||
|
||||
func testShadowsocks2022Tcp(t *testing.T, method string, password string) {
|
||||
|
||||
@@ -65,7 +65,6 @@ func TestWireguard(t *testing.T) {
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -105,7 +104,6 @@ func TestWireguard(t *testing.T) {
|
||||
AllowedIps: []string{"0.0.0.0/0", "::0/0"},
|
||||
}},
|
||||
}),
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,291 +2,103 @@ package finalmask
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
type Dialer struct {
|
||||
DialTCP func(net.Destination) (net.Conn, error)
|
||||
DialUDP func(net.Destination) (net.Conn, error)
|
||||
type Udpmask interface {
|
||||
WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error)
|
||||
WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error)
|
||||
}
|
||||
|
||||
type ListenConfig struct {
|
||||
Listen func(net.Addr) (net.Listener, error)
|
||||
ListenPacket func(net.Addr) (net.PacketConn, error)
|
||||
type UdpmaskManager struct {
|
||||
udpmasks []Udpmask
|
||||
}
|
||||
|
||||
type TCPMask interface {
|
||||
WrapConnClient(net.Conn, *net.Destination, *Dialer) (net.Conn, error)
|
||||
WrapConnServer(net.Conn) (net.Conn, error)
|
||||
// Listen(net.Listener) (net.Listener, error)
|
||||
func NewUdpmaskManager(udpmasks []Udpmask) *UdpmaskManager {
|
||||
slices.Reverse(udpmasks)
|
||||
return &UdpmaskManager{udpmasks: udpmasks}
|
||||
}
|
||||
|
||||
type UDPMask interface {
|
||||
WrapPacketConnClient(net.PacketConn, *net.Destination, *Dialer) (net.PacketConn, error)
|
||||
WrapPacketConnServer(net.PacketConn, net.Addr, *ListenConfig) (net.PacketConn, error)
|
||||
}
|
||||
|
||||
type FinalMask struct {
|
||||
tcpMasks []TCPMask
|
||||
udpMasks []UDPMask
|
||||
dialTCP func(context.Context, net.Destination) (net.Conn, error)
|
||||
listen func(context.Context, net.Addr) (net.Listener, error)
|
||||
dialUDP func(context.Context, net.Destination) (net.PacketConn, net.Addr, error)
|
||||
listenPacket func(context.Context, net.Addr) (net.PacketConn, error)
|
||||
}
|
||||
|
||||
func NewFinalMask(tcpMasks []TCPMask, udpMasks []UDPMask, dialTCP func(context.Context, net.Destination) (net.Conn, error), listen func(context.Context, net.Addr) (net.Listener, error), dialUDP func(context.Context, net.Destination) (net.PacketConn, net.Addr, error), listenPacket func(context.Context, net.Addr) (net.PacketConn, error)) *FinalMask {
|
||||
slices.Reverse(tcpMasks)
|
||||
slices.Reverse(udpMasks)
|
||||
return &FinalMask{
|
||||
tcpMasks: tcpMasks,
|
||||
udpMasks: udpMasks,
|
||||
dialTCP: dialTCP,
|
||||
dialUDP: dialUDP,
|
||||
listen: listen,
|
||||
listenPacket: listenPacket,
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FinalMask) DialTCP(ctx context.Context, dest net.Destination) (net.Conn, error) {
|
||||
if len(fm.tcpMasks) == 0 {
|
||||
return fm.dialTCP(ctx, dest)
|
||||
}
|
||||
for i := range fm.tcpMasks {
|
||||
if i > 0 {
|
||||
if _, ok := fm.tcpMasks[i].(interface{ HandleDial() }); ok {
|
||||
return nil, fmt.Errorf("incorrect index: %d %T", i, fm.tcpMasks[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if _, ok := fm.tcpMasks[0].(interface{ HandleDial() }); !ok {
|
||||
conn, err = fm.dialTCP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
dialer := &Dialer{
|
||||
DialTCP: func(dest net.Destination) (net.Conn, error) {
|
||||
return fm.dialTCP(ctx, dest)
|
||||
},
|
||||
DialUDP: func(dest net.Destination) (net.Conn, error) {
|
||||
conn, addr, err := fm.dialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PacketConnWrapper{PacketConn: conn, udpAddr: addr}, err
|
||||
},
|
||||
}
|
||||
for i := range fm.tcpMasks {
|
||||
var newConn net.Conn
|
||||
newConn, err = fm.tcpMasks[i].WrapConnClient(conn, &dest, dialer)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = newConn
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (fm *FinalMask) Listen(ctx context.Context, addr net.Addr) (net.Listener, error) {
|
||||
if len(fm.tcpMasks) == 0 {
|
||||
return fm.listen(ctx, addr)
|
||||
}
|
||||
off := 0
|
||||
listener, err := fm.listen(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range fm.tcpMasks {
|
||||
if _, ok := fm.tcpMasks[i].(interface {
|
||||
Listen(net.Listener) (net.Listener, error)
|
||||
}); ok {
|
||||
if i-off == 0 {
|
||||
l, err := fm.tcpMasks[i].(interface {
|
||||
Listen(net.Listener) (net.Listener, error)
|
||||
}).Listen(listener)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return nil, err
|
||||
}
|
||||
listener = l
|
||||
} else {
|
||||
l, err := fm.tcpMasks[i].(interface {
|
||||
Listen(net.Listener) (net.Listener, error)
|
||||
}).Listen(&TCPListener{Listener: listener, tcpMasks: fm.tcpMasks[off:i]})
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return nil, err
|
||||
}
|
||||
listener = l
|
||||
}
|
||||
off = i + 1
|
||||
}
|
||||
}
|
||||
if off < len(fm.tcpMasks) {
|
||||
return &TCPListener{Listener: listener, tcpMasks: fm.tcpMasks[off:]}, nil
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (fm *FinalMask) DialUDP(ctx context.Context, dest net.Destination) (net.Conn, error) {
|
||||
if len(fm.udpMasks) == 0 {
|
||||
conn, addr, err := fm.dialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PacketConnWrapper{PacketConn: conn, udpAddr: addr}, nil
|
||||
}
|
||||
for i := range fm.udpMasks {
|
||||
if i > 0 {
|
||||
if _, ok := fm.udpMasks[i].(interface{ HandleDial() }); ok {
|
||||
return nil, fmt.Errorf("incorrect index: %d %T", i, fm.udpMasks[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
var conn net.PacketConn
|
||||
var addr net.Addr
|
||||
var err error
|
||||
if _, ok := fm.udpMasks[0].(interface{ HandleDial() }); !ok {
|
||||
conn, addr, err = fm.dialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
dialer := &Dialer{
|
||||
DialTCP: func(dest net.Destination) (net.Conn, error) {
|
||||
return fm.dialTCP(ctx, dest)
|
||||
},
|
||||
DialUDP: func(dest net.Destination) (net.Conn, error) {
|
||||
conn, addr, err := fm.dialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PacketConnWrapper{PacketConn: conn, udpAddr: addr}, err
|
||||
},
|
||||
}
|
||||
func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketConn, error) {
|
||||
var sizes []int
|
||||
var conns []net.PacketConn
|
||||
for i := range fm.udpMasks {
|
||||
var newConn net.PacketConn
|
||||
if _, ok := fm.udpMasks[i].(interface{ HeaderConn() }); ok {
|
||||
newConn, err = fm.udpMasks[i].WrapPacketConnClient(nil, nil, nil)
|
||||
for i, mask := range m.udpmasks {
|
||||
if _, ok := mask.(headerConn); ok {
|
||||
conn, err := mask.WrapPacketConnClient(nil, i, len(m.udpmasks)-1)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
sizes = append(sizes, newConn.(interface{ Size() int }).Size())
|
||||
conns = append(conns, newConn)
|
||||
sizes = append(sizes, conn.(headerSize).Size())
|
||||
conns = append(conns, conn)
|
||||
} else {
|
||||
if len(conns) > 0 {
|
||||
conn = &headerManagerConn{PacketConn: conn, sizes: sizes, conns: conns}
|
||||
raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw}
|
||||
sizes = nil
|
||||
conns = nil
|
||||
}
|
||||
newConn, err = fm.udpMasks[i].WrapPacketConnClient(conn, &dest, dialer)
|
||||
var err error
|
||||
raw, err = mask.WrapPacketConnClient(raw, i, len(m.udpmasks)-1)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = newConn
|
||||
}
|
||||
}
|
||||
|
||||
if len(conns) > 0 {
|
||||
conn = &headerManagerConn{PacketConn: conn, sizes: sizes, conns: conns}
|
||||
raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw}
|
||||
sizes = nil
|
||||
conns = nil
|
||||
}
|
||||
if addr == nil {
|
||||
addr = &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
|
||||
}
|
||||
return &PacketConnWrapper{PacketConn: conn, udpAddr: addr}, nil
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func (fm *FinalMask) ListenPacket(ctx context.Context, addr net.Addr) (net.PacketConn, error) {
|
||||
if len(fm.udpMasks) == 0 {
|
||||
return fm.listenPacket(ctx, addr)
|
||||
}
|
||||
for i := range fm.udpMasks {
|
||||
if i > 0 {
|
||||
if _, ok := fm.udpMasks[i].(interface{ HandleListen() }); ok {
|
||||
return nil, fmt.Errorf("incorrect index: %d %T", i, fm.udpMasks[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
var conn net.PacketConn
|
||||
var err error
|
||||
if _, ok := fm.udpMasks[0].(interface{ HandleListen() }); !ok {
|
||||
conn, err = fm.listenPacket(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
lc := &ListenConfig{
|
||||
Listen: func(addr net.Addr) (net.Listener, error) { return fm.listen(ctx, addr) },
|
||||
ListenPacket: func(addr net.Addr) (net.PacketConn, error) { return fm.listenPacket(ctx, addr) },
|
||||
}
|
||||
func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketConn, error) {
|
||||
var sizes []int
|
||||
var conns []net.PacketConn
|
||||
for i := range fm.udpMasks {
|
||||
var newConn net.PacketConn
|
||||
if _, ok := fm.udpMasks[i].(interface{ HeaderConn() }); ok {
|
||||
newConn, err = fm.udpMasks[i].WrapPacketConnServer(nil, nil, nil)
|
||||
for i, mask := range m.udpmasks {
|
||||
if _, ok := mask.(headerConn); ok {
|
||||
conn, err := mask.WrapPacketConnServer(nil, i, len(m.udpmasks)-1)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
sizes = append(sizes, newConn.(interface{ Size() int }).Size())
|
||||
conns = append(conns, newConn)
|
||||
sizes = append(sizes, conn.(headerSize).Size())
|
||||
conns = append(conns, conn)
|
||||
} else {
|
||||
if len(conns) > 0 {
|
||||
conn = &headerManagerConn{PacketConn: conn, sizes: sizes, conns: conns}
|
||||
raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw}
|
||||
sizes = nil
|
||||
conns = nil
|
||||
}
|
||||
newConn, err = fm.udpMasks[i].WrapPacketConnServer(conn, addr, lc)
|
||||
var err error
|
||||
raw, err = mask.WrapPacketConnServer(raw, i, len(m.udpmasks)-1)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = newConn
|
||||
}
|
||||
}
|
||||
|
||||
if len(conns) > 0 {
|
||||
conn = &headerManagerConn{PacketConn: conn, sizes: sizes, conns: conns}
|
||||
raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw}
|
||||
sizes = nil
|
||||
conns = nil
|
||||
}
|
||||
return conn, nil
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
const (
|
||||
UDPSize = 4096
|
||||
)
|
||||
|
||||
type PacketConnWrapper struct {
|
||||
net.PacketConn
|
||||
udpAddr net.Addr
|
||||
type headerConn interface {
|
||||
HeaderConn()
|
||||
}
|
||||
|
||||
func (c *PacketConnWrapper) RemoteAddr() net.Addr {
|
||||
return c.udpAddr
|
||||
}
|
||||
|
||||
func (c *PacketConnWrapper) Read(b []byte) (n int, err error) {
|
||||
n, _, err = c.PacketConn.ReadFrom(b)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *PacketConnWrapper) Write(b []byte) (n int, err error) {
|
||||
return c.PacketConn.WriteTo(b, c.udpAddr)
|
||||
type headerSize interface {
|
||||
Size() int
|
||||
}
|
||||
|
||||
type headerManagerConn struct {
|
||||
@@ -379,27 +191,72 @@ func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type TCPListener struct {
|
||||
net.Listener
|
||||
tcpMasks []TCPMask
|
||||
type Tcpmask interface {
|
||||
WrapConnClient(net.Conn) (net.Conn, error)
|
||||
WrapConnServer(net.Conn) (net.Conn, error)
|
||||
}
|
||||
|
||||
func (l *TCPListener) Accept() (net.Conn, error) {
|
||||
type TcpmaskManager struct {
|
||||
tcpmasks []Tcpmask
|
||||
}
|
||||
|
||||
func NewTcpmaskManager(tcpmasks []Tcpmask) *TcpmaskManager {
|
||||
slices.Reverse(tcpmasks)
|
||||
return &TcpmaskManager{tcpmasks: tcpmasks}
|
||||
}
|
||||
|
||||
func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
var err error
|
||||
for _, mask := range m.tcpmasks {
|
||||
raw, err = mask.WrapConnClient(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func (m *TcpmaskManager) WrapConnServer(raw net.Conn) (net.Conn, error) {
|
||||
var err error
|
||||
for _, mask := range m.tcpmasks {
|
||||
raw, err = mask.WrapConnServer(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func (m *TcpmaskManager) WrapListener(l net.Listener) (net.Listener, error) {
|
||||
return NewTcpListener(m, l)
|
||||
}
|
||||
|
||||
type tcpListener struct {
|
||||
m *TcpmaskManager
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func NewTcpListener(m *TcpmaskManager, l net.Listener) (net.Listener, error) {
|
||||
return &tcpListener{
|
||||
m: m,
|
||||
Listener: l,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *tcpListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
for i := range l.tcpMasks {
|
||||
var newConn net.Conn
|
||||
newConn, err = l.tcpMasks[i].WrapConnServer(conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = newConn
|
||||
newConn, err := l.m.WrapConnServer(conn)
|
||||
if err != nil {
|
||||
errors.LogDebugInner(context.Background(), err, "mask err")
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
|
||||
return newConn, nil
|
||||
}
|
||||
|
||||
type TcpMaskConn interface {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package fragment
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
import "net"
|
||||
|
||||
func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
|
||||
return NewConnClient(c, conn, false)
|
||||
func (c *Config) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
return NewConnClient(c, raw, false)
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
return NewConnServer(c, conn, true)
|
||||
func (c *Config) WrapConnServer(raw net.Conn) (net.Conn, error) {
|
||||
return NewConnServer(c, raw, true)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
package custom
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *TCPConfig) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
|
||||
return NewConnClientTCP(c, conn)
|
||||
func (c *TCPConfig) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
return NewConnClientTCP(c, raw)
|
||||
}
|
||||
|
||||
func (c *TCPConfig) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
return NewConnServerTCP(c, conn)
|
||||
func (c *TCPConfig) WrapConnServer(raw net.Conn) (net.Conn, error) {
|
||||
return NewConnServerTCP(c, raw)
|
||||
}
|
||||
|
||||
func (c *UDPConfig) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClientUDP(c, conn)
|
||||
func (c *UDPConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClientUDP(c, raw)
|
||||
}
|
||||
|
||||
func (c *UDPConfig) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServerUDP(c, conn)
|
||||
func (c *UDPConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnServerUDP(c, raw)
|
||||
}
|
||||
|
||||
func (c *UDPStandaloneConfig) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClientUDPStandalone(c, conn)
|
||||
func (c *UDPStandaloneConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClientUDPStandalone(c, raw)
|
||||
}
|
||||
|
||||
func (c *UDPStandaloneConfig) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServerUDPStandalone(c, conn)
|
||||
func (c *UDPStandaloneConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnServerUDPStandalone(c, raw)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
|
||||
func TestMetadataEvaluatorRejectsUnknownName(t *testing.T) {
|
||||
@@ -154,7 +156,7 @@ func TestMetadataUDPStandaloneWriteUsesRemotePort(t *testing.T) {
|
||||
}
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := cfg.WrapPacketConnClient(clientRaw, nil, nil)
|
||||
client, err := finalmask.NewUdpmaskManager([]finalmask.Udpmask{cfg}).WrapPacketConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -299,7 +301,7 @@ func TestMetadataTCPHandshakeUsesEndpointPorts(t *testing.T) {
|
||||
}
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := clientCfg.WrapConnClient(clientRaw, nil, nil)
|
||||
client, err := clientCfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
|
||||
func mustSendRecvUDP(t *testing.T, from net.PacketConn, to net.PacketConn, msg []byte) {
|
||||
@@ -46,6 +48,7 @@ func TestStateUDPResponseReusesPriorCapturedValues(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
maskManager := finalmask.NewUdpmaskManager([]finalmask.Udpmask{cfg})
|
||||
|
||||
clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -59,11 +62,11 @@ func TestStateUDPResponseReusesPriorCapturedValues(t *testing.T) {
|
||||
}
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := cfg.WrapPacketConnClient(clientRaw, nil, nil)
|
||||
client, err := maskManager.WrapPacketConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, err := cfg.WrapPacketConnServer(serverRaw, nil, nil)
|
||||
server, err := maskManager.WrapPacketConnServer(serverRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestDSLTCPHandshakeReusesCapturedValue(t *testing.T) {
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
client, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestDSLTCPClientRejectsMismatchedResponseSequence(t *testing.T) {
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := clientCfg.WrapConnClient(clientRaw, nil, nil)
|
||||
client, err := clientCfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package aes128gcm
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClient(c, conn)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServer(c, conn)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package header
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClient(c, conn)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServer(c, conn)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package original
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClient(c, conn)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServer(c, conn)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
import "net"
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClient(c, conn)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServer(c, conn)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package realm
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClient(c, conn)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
_, ok1 := raw.(*internet.FakePacketConn)
|
||||
if level != 0 || ok1 {
|
||||
return nil, errors.New("realm requires being at the outermost level")
|
||||
}
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServer(c, conn)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
if level != 0 {
|
||||
return nil, errors.New("realm requires being at the outermost level")
|
||||
}
|
||||
return NewConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
package salamander
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewSalamanderConnClient(c, conn)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewSalamanderConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewSalamanderConnServer(c, conn)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewSalamanderConnServer(c, raw)
|
||||
}
|
||||
|
||||
func (c *GeckoConfig) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewGeckoConnClient(c, conn)
|
||||
func (c *GeckoConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewGeckoConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *GeckoConfig) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewGeckoConnServer(c, conn)
|
||||
func (c *GeckoConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewGeckoConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
// Sudoku in finalmask mode is a pure appearance transform with no standalone handshake.
|
||||
// TCP always keeps classic sudoku on uplink and uses packed downlink optimization on server writes.
|
||||
func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
|
||||
return newPackedDirectionalConn(conn, c, true)
|
||||
func (c *Config) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
return newPackedDirectionalConn(raw, c, true)
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
return newPackedDirectionalConn(conn, c, false)
|
||||
func (c *Config) WrapConnServer(raw net.Conn) (net.Conn, error) {
|
||||
return newPackedDirectionalConn(raw, c, false)
|
||||
}
|
||||
|
||||
func newPackedDirectionalConn(raw net.Conn, config *Config, readPacked bool) (net.Conn, error) {
|
||||
@@ -35,10 +36,16 @@ func newPackedDirectionalConn(raw net.Conn, config *Config, readPacked bool) (ne
|
||||
return newWrappedConn(raw, reader, writer), nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewUDPConn(conn, c)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
if level != levelCount {
|
||||
return nil, errors.New("sudoku udp mask must be the innermost mask in chain")
|
||||
}
|
||||
return NewUDPConn(raw, c)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewUDPConn(conn, c)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
if level != levelCount {
|
||||
return nil, errors.New("sudoku udp mask must be the innermost mask in chain")
|
||||
}
|
||||
return NewUDPConn(raw, c)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ package finalmask_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
gonet "net"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
)
|
||||
@@ -22,14 +20,11 @@ func mustSendRecvTcp(
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
waitCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
_, err := from.Write(msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
@@ -45,23 +40,18 @@ func mustSendRecvTcp(
|
||||
if !bytes.Equal(buf[:n], msg) {
|
||||
t.Fatalf("unexpected data %q", buf[:n])
|
||||
}
|
||||
|
||||
<-waitCh
|
||||
}
|
||||
|
||||
type layerMaskTcp struct {
|
||||
name string
|
||||
mask finalmask.TCPMask
|
||||
mask finalmask.Tcpmask
|
||||
}
|
||||
|
||||
type failingWrapMask struct{}
|
||||
|
||||
func (failingWrapMask) TCP() {}
|
||||
func (f failingWrapMask) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (f failingWrapMask) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
func (failingWrapMask) TCP() {}
|
||||
func (f failingWrapMask) WrapConnClient(raw net.Conn) (net.Conn, error) { return raw, nil }
|
||||
func (f failingWrapMask) WrapConnServer(raw net.Conn) (net.Conn, error) {
|
||||
return nil, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
@@ -102,31 +92,32 @@ func TestConnReadWrite(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
mask := c.mask
|
||||
|
||||
dialTCP := func(ctx context.Context, dest net.Destination) (net.Conn, error) {
|
||||
return net.Dial("tcp", dest.NetAddr())
|
||||
}
|
||||
listen := func(ctx context.Context, addr net.Addr) (net.Listener, error) {
|
||||
return net.Listen("tcp", addr.String())
|
||||
}
|
||||
finalMask := finalmask.NewFinalMask([]finalmask.TCPMask{mask}, nil, dialTCP, listen, nil, nil)
|
||||
maskManager := finalmask.NewTcpmaskManager([]finalmask.Tcpmask{mask})
|
||||
|
||||
listener, err := finalMask.Listen(context.Background(), &net.TCPAddr{IP: net.LocalHostIP.IP()})
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { listener.Close() })
|
||||
|
||||
client, err := finalMask.DialTCP(context.Background(), net.TCPDestination(net.IPAddress(listener.Addr().(*net.TCPAddr).IP), net.Port(listener.Addr().(*net.TCPAddr).Port)))
|
||||
client, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { client.Close() })
|
||||
|
||||
server, err := listener.Accept()
|
||||
client, err = maskManager.WrapConnClient(client)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server, err = maskManager.WrapConnServer(server)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { server.Close() })
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(time.Second))
|
||||
_ = server.SetDeadline(time.Now().Add(time.Second))
|
||||
@@ -159,32 +150,34 @@ func TestTCPcustomStaticHandshakeRoundTrip(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
maskManager := finalmask.NewTcpmaskManager([]finalmask.Tcpmask{cfg})
|
||||
|
||||
dialTCP := func(ctx context.Context, dest net.Destination) (net.Conn, error) {
|
||||
return net.Dial("tcp", dest.NetAddr())
|
||||
}
|
||||
listen := func(ctx context.Context, addr net.Addr) (net.Listener, error) {
|
||||
return net.Listen("tcp", addr.String())
|
||||
}
|
||||
finalMask := finalmask.NewFinalMask([]finalmask.TCPMask{cfg}, nil, dialTCP, listen, nil, nil)
|
||||
|
||||
listener, err := finalMask.Listen(context.Background(), &net.TCPAddr{IP: net.LocalHostIP.IP()})
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
defer ln.Close()
|
||||
|
||||
client, err := finalMask.DialTCP(context.Background(), net.TCPDestination(net.IPAddress(listener.Addr().(*net.TCPAddr).IP), net.Port(listener.Addr().(*net.TCPAddr).Port)))
|
||||
clientRaw, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer clientRaw.Close()
|
||||
|
||||
server, err := listener.Accept()
|
||||
serverRaw, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := maskManager.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, err := maskManager.WrapConnServer(serverRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(time.Second))
|
||||
_ = server.SetDeadline(time.Now().Add(time.Second))
|
||||
@@ -227,11 +220,11 @@ func TestTCPcustomClientRejectsMismatchedServerSequence(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := clientCfg.WrapConnClient(clientRaw, nil, nil)
|
||||
client, err := clientCfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -264,37 +257,42 @@ func TestTCPcustomClientRejectsMismatchedServerSequence(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTCPWrapListenerRejectsImmediateWrapErrors(t *testing.T) {
|
||||
dialTCP := func(ctx context.Context, dest net.Destination) (net.Conn, error) {
|
||||
return net.Dial("tcp", dest.NetAddr())
|
||||
}
|
||||
listen := func(ctx context.Context, addr net.Addr) (net.Listener, error) {
|
||||
return net.Listen("tcp", addr.String())
|
||||
}
|
||||
finalMask := finalmask.NewFinalMask([]finalmask.TCPMask{failingWrapMask{}}, nil, dialTCP, listen, nil, nil)
|
||||
clientManager := finalmask.NewTcpmaskManager([]finalmask.Tcpmask{failingWrapMask{}})
|
||||
serverManager := finalmask.NewTcpmaskManager([]finalmask.Tcpmask{failingWrapMask{}})
|
||||
|
||||
listener, err := finalMask.Listen(context.Background(), &net.TCPAddr{IP: net.LocalHostIP.IP()})
|
||||
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rawLn.Close()
|
||||
|
||||
ln, err := serverManager.WrapListener(rawLn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
conn, err := ln.Accept()
|
||||
accepted <- struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}{conn: conn, err: err}
|
||||
}()
|
||||
|
||||
client, err := finalMask.DialTCP(context.Background(), net.TCPDestination(net.IPAddress(listener.Addr().(*net.TCPAddr).IP), net.Port(listener.Addr().(*net.TCPAddr).Port)))
|
||||
clientRaw, err := net.Dial("tcp", rawLn.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := clientManager.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(time.Second))
|
||||
|
||||
|
||||
@@ -2,15 +2,13 @@ package finalmask_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
gonet "net"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/proxy"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
@@ -53,7 +51,7 @@ func mustSendRecv(
|
||||
|
||||
type layerMask struct {
|
||||
name string
|
||||
mask finalmask.UDPMask
|
||||
mask finalmask.Udpmask
|
||||
layers int
|
||||
}
|
||||
|
||||
@@ -215,23 +213,25 @@ func newStandaloneStunLikeUDPServerConfig() *custom.UDPStandaloneConfig {
|
||||
func newUDPClientServerPair(t *testing.T, cfg *custom.UDPStandaloneConfig) (net.PacketConn, net.PacketConn, net.PacketConn, net.PacketConn) {
|
||||
t.Helper()
|
||||
|
||||
clientRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
|
||||
clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = clientRaw.Close() })
|
||||
|
||||
serverRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
|
||||
serverRaw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = serverRaw.Close() })
|
||||
|
||||
client, err := cfg.WrapPacketConnClient(clientRaw, nil, nil)
|
||||
maskManager := finalmask.NewUdpmaskManager([]finalmask.Udpmask{cfg})
|
||||
|
||||
client, err := maskManager.WrapPacketConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, err := cfg.WrapPacketConnServer(serverRaw, nil, nil)
|
||||
server, err := maskManager.WrapPacketConnServer(serverRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -348,39 +348,31 @@ func TestPacketConnReadWrite(t *testing.T) {
|
||||
if layers <= 0 {
|
||||
layers = 1
|
||||
}
|
||||
masks := make([]finalmask.UDPMask, 0, layers)
|
||||
masks := make([]finalmask.Udpmask, 0, layers)
|
||||
for i := 0; i < layers; i++ {
|
||||
masks = append(masks, mask)
|
||||
}
|
||||
maskManager := finalmask.NewUdpmaskManager(masks)
|
||||
|
||||
dialUDP := func(ctx context.Context, dest net.Destination) (net.PacketConn, net.Addr, error) {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", dest.NetAddr())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conn, err := gonet.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return conn, udpAddr, nil
|
||||
}
|
||||
listenPacket := func(ctx context.Context, addr net.Addr) (net.PacketConn, error) {
|
||||
return gonet.ListenPacket(addr.Network(), addr.String())
|
||||
}
|
||||
finalMask := finalmask.NewFinalMask(nil, masks, nil, nil, dialUDP, listenPacket)
|
||||
|
||||
server, err := finalMask.ListenPacket(context.Background(), &net.UDPAddr{IP: net.LocalHostIP.IP()})
|
||||
client, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { server.Close() })
|
||||
|
||||
clientConn, err := finalMask.DialUDP(context.Background(), net.UDPDestination(net.IPAddress(server.LocalAddr().(*net.UDPAddr).IP), net.Port(server.LocalAddr().(*net.UDPAddr).Port)))
|
||||
client, err = maskManager.WrapPacketConnClient(client)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server, err = maskManager.WrapPacketConnServer(server)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { clientConn.Close() })
|
||||
client := clientConn.(*finalmask.PacketConnWrapper).PacketConn
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(time.Second))
|
||||
_ = server.SetDeadline(time.Now().Add(time.Second))
|
||||
@@ -405,20 +397,21 @@ func TestUDPcustomStaticHeaderWireShape(t *testing.T) {
|
||||
{Rand: 1, RandMin: 0x30, RandMax: 0x40},
|
||||
},
|
||||
}
|
||||
maskManager := finalmask.NewUdpmaskManager([]finalmask.Udpmask{cfg})
|
||||
|
||||
clientRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
|
||||
clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
serverRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
|
||||
serverRaw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := cfg.WrapPacketConnClient(clientRaw, nil, nil)
|
||||
client, err := maskManager.WrapPacketConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -649,11 +642,11 @@ func TestSudokuBDD(t *testing.T) {
|
||||
Ascii: "prefer_ascii",
|
||||
}
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -690,11 +683,11 @@ func TestSudokuBDD(t *testing.T) {
|
||||
PaddingMax: 0,
|
||||
}
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -745,10 +738,10 @@ func TestSudokuBDD(t *testing.T) {
|
||||
countWireBytes := func(wrapServer func(net.Conn, *sudoku.Config) (net.Conn, error), cfg *sudoku.Config) int64 {
|
||||
t.Helper()
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
watchedServerRaw := &countingConn{Conn: serverRaw}
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -800,11 +793,11 @@ func TestSudokuBDD(t *testing.T) {
|
||||
CustomTables: []string{"xpxvvpvv", "vxpvxvvp"},
|
||||
}
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -842,11 +835,11 @@ func TestSudokuBDD(t *testing.T) {
|
||||
PaddingMax: 0,
|
||||
}
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -875,6 +868,19 @@ func TestSudokuBDD(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GivenSudokuUDPMask_WhenNotInnermost_ThenWrapFails", func(t *testing.T) {
|
||||
cfg := &sudoku.Config{Password: "sudoku-udp"}
|
||||
raw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer raw.Close()
|
||||
|
||||
if _, err := cfg.WrapPacketConnClient(raw, 0, 1); err == nil {
|
||||
t.Fatal("expected innermost check failure")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GivenSudokuMultiTableUDPMask_WhenClientSendsMultipleDatagrams_ThenPayloadMatches", func(t *testing.T) {
|
||||
cfg := &sudoku.Config{
|
||||
Password: "sudoku-udp-multi",
|
||||
@@ -883,24 +889,25 @@ func TestSudokuBDD(t *testing.T) {
|
||||
PaddingMin: 0,
|
||||
PaddingMax: 0,
|
||||
}
|
||||
maskManager := finalmask.NewUdpmaskManager([]finalmask.Udpmask{cfg})
|
||||
|
||||
clientRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
|
||||
clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
serverRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
|
||||
serverRaw, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer serverRaw.Close()
|
||||
|
||||
client, err := cfg.WrapPacketConnClient(clientRaw, nil, nil)
|
||||
client, err := maskManager.WrapPacketConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, err := cfg.WrapPacketConnServer(serverRaw, nil, nil)
|
||||
server, err := maskManager.WrapPacketConnServer(serverRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -954,7 +961,7 @@ func TestSudokuBDD(t *testing.T) {
|
||||
}
|
||||
defer serverRaw.Close()
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1001,11 +1008,11 @@ func TestSudokuBDD(t *testing.T) {
|
||||
Ascii: "prefer_entropy",
|
||||
}
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1025,11 +1032,11 @@ func TestSudokuBDD(t *testing.T) {
|
||||
Ascii: "prefer_entropy",
|
||||
}
|
||||
|
||||
clientRaw, serverRaw := gonet.Pipe()
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw, nil, nil)
|
||||
clientConn, err := cfg.WrapConnClient(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package udphop
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func (c *Config) HandleDial() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewUDPHopConn(c, dest, dialer)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
_, ok1 := raw.(*internet.FakePacketConn)
|
||||
if level != 0 || ok1 {
|
||||
return nil, errors.New("udphop requires being at the outermost level")
|
||||
}
|
||||
return NewUDPHopConn(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return nil, errors.New("udphop: client only")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package udphop
|
||||
|
||||
import (
|
||||
internet "github.com/xtls/xray-core/transport/internet"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
@@ -23,13 +24,14 @@ const (
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Sockopt *internet.SocketConfig `protobuf:"bytes,1,opt,name=sockopt,proto3" json:"sockopt,omitempty"`
|
||||
Local bool `protobuf:"varint,2,opt,name=local,proto3" json:"local,omitempty"`
|
||||
Remote bool `protobuf:"varint,3,opt,name=remote,proto3" json:"remote,omitempty"`
|
||||
RemoteOnce bool `protobuf:"varint,4,opt,name=remote_once,json=remoteOnce,proto3" json:"remote_once,omitempty"`
|
||||
IntervalMin int64 `protobuf:"varint,5,opt,name=interval_min,json=intervalMin,proto3" json:"interval_min,omitempty"`
|
||||
IntervalMax int64 `protobuf:"varint,6,opt,name=interval_max,json=intervalMax,proto3" json:"interval_max,omitempty"`
|
||||
RemoteIPs []string `protobuf:"bytes,7,rep,name=remoteIPs,proto3" json:"remoteIPs,omitempty"`
|
||||
RemotePorts []uint32 `protobuf:"varint,8,rep,packed,name=remote_ports,json=remotePorts,proto3" json:"remote_ports,omitempty"`
|
||||
RemotePorts []uint32 `protobuf:"varint,7,rep,packed,name=remote_ports,json=remotePorts,proto3" json:"remote_ports,omitempty"`
|
||||
RemoteIPs []string `protobuf:"bytes,8,rep,name=remoteIPs,proto3" json:"remoteIPs,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -64,6 +66,13 @@ func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_udphop_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetSockopt() *internet.SocketConfig {
|
||||
if x != nil {
|
||||
return x.Sockopt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetLocal() bool {
|
||||
if x != nil {
|
||||
return x.Local
|
||||
@@ -99,16 +108,16 @@ func (x *Config) GetIntervalMax() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetRemoteIPs() []string {
|
||||
func (x *Config) GetRemotePorts() []uint32 {
|
||||
if x != nil {
|
||||
return x.RemoteIPs
|
||||
return x.RemotePorts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetRemotePorts() []uint32 {
|
||||
func (x *Config) GetRemoteIPs() []string {
|
||||
if x != nil {
|
||||
return x.RemotePorts
|
||||
return x.RemoteIPs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -117,16 +126,17 @@ var File_transport_internet_finalmask_udphop_config_proto protoreflect.FileDescr
|
||||
|
||||
const file_transport_internet_finalmask_udphop_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"0transport/internet/finalmask/udphop/config.proto\x12(xray.transport.internet.finalmask.udphop\"\xe4\x01\n" +
|
||||
"\x06Config\x12\x14\n" +
|
||||
"0transport/internet/finalmask/udphop/config.proto\x12(xray.transport.internet.finalmask.udphop\x1a\x1ftransport/internet/config.proto\"\x9f\x02\n" +
|
||||
"\x06Config\x12?\n" +
|
||||
"\asockopt\x18\x01 \x01(\v2%.xray.transport.internet.SocketConfigR\asockopt\x12\x14\n" +
|
||||
"\x05local\x18\x02 \x01(\bR\x05local\x12\x16\n" +
|
||||
"\x06remote\x18\x03 \x01(\bR\x06remote\x12\x1f\n" +
|
||||
"\vremote_once\x18\x04 \x01(\bR\n" +
|
||||
"remoteOnce\x12!\n" +
|
||||
"\finterval_min\x18\x05 \x01(\x03R\vintervalMin\x12!\n" +
|
||||
"\finterval_max\x18\x06 \x01(\x03R\vintervalMax\x12\x1c\n" +
|
||||
"\tremoteIPs\x18\a \x03(\tR\tremoteIPs\x12!\n" +
|
||||
"\fremote_ports\x18\b \x03(\rR\vremotePortsJ\x04\b\x01\x10\x02B\x9a\x01\n" +
|
||||
"\finterval_max\x18\x06 \x01(\x03R\vintervalMax\x12!\n" +
|
||||
"\fremote_ports\x18\a \x03(\rR\vremotePorts\x12\x1c\n" +
|
||||
"\tremoteIPs\x18\b \x03(\tR\tremoteIPsB\x9a\x01\n" +
|
||||
",com.xray.transport.internet.finalmask.udphopP\x01Z=github.com/xtls/xray-core/transport/internet/finalmask/udphop\xaa\x02(Xray.Transport.Internet.Finalmask.Udphopb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -143,14 +153,16 @@ func file_transport_internet_finalmask_udphop_config_proto_rawDescGZIP() []byte
|
||||
|
||||
var file_transport_internet_finalmask_udphop_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_transport_internet_finalmask_udphop_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.finalmask.udphop.Config
|
||||
(*Config)(nil), // 0: xray.transport.internet.finalmask.udphop.Config
|
||||
(*internet.SocketConfig)(nil), // 1: xray.transport.internet.SocketConfig
|
||||
}
|
||||
var file_transport_internet_finalmask_udphop_config_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
1, // 0: xray.transport.internet.finalmask.udphop.Config.sockopt:type_name -> xray.transport.internet.SocketConfig
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_udphop_config_proto_init() }
|
||||
|
||||
@@ -6,14 +6,16 @@ option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/udph
|
||||
option java_package = "com.xray.transport.internet.finalmask.udphop";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "transport/internet/config.proto";
|
||||
|
||||
message Config {
|
||||
reserved 1;
|
||||
xray.transport.internet.SocketConfig sockopt = 1;
|
||||
bool local = 2;
|
||||
bool remote = 3;
|
||||
bool remote_once = 4;
|
||||
int64 interval_min = 5;
|
||||
int64 interval_max = 6;
|
||||
repeated string remoteIPs = 7;
|
||||
repeated uint32 remote_ports = 8;
|
||||
repeated uint32 remote_ports = 7;
|
||||
repeated string remoteIPs = 8;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
goerrors "errors"
|
||||
"io"
|
||||
mrand "math/rand"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,8 @@ import (
|
||||
"github.com/xtls/xray-core/common/crypto"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/net/cnc"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
|
||||
@@ -30,14 +34,16 @@ type packet struct {
|
||||
}
|
||||
|
||||
type udpHopConn struct {
|
||||
dialer *finalmask.Dialer
|
||||
local bool
|
||||
remote bool
|
||||
conn net.PacketConn
|
||||
sockopt *internet.SocketConfig
|
||||
local bool
|
||||
remote bool
|
||||
remoteOnce bool
|
||||
|
||||
intervalMin int64
|
||||
intervalMax int64
|
||||
remoteIPs []netip.Prefix
|
||||
remotePorts []uint32
|
||||
remoteIPs []netip.Prefix
|
||||
|
||||
deadline time.Time
|
||||
readDeadline time.Time
|
||||
@@ -49,10 +55,10 @@ type udpHopConn struct {
|
||||
readCh chan packet
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
mu sync.RWMutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewUDPHopConn(c *Config, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
func NewUDPHopConn(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
if c.IntervalMin < 5 || c.IntervalMax < 5 {
|
||||
return nil, errors.New("invalid interval")
|
||||
}
|
||||
@@ -60,40 +66,22 @@ func NewUDPHopConn(c *Config, dest *net.Destination, dialer *finalmask.Dialer) (
|
||||
for _, ip := range c.RemoteIPs {
|
||||
remoteIPs = append(remoteIPs, netip.MustParsePrefix(ip))
|
||||
}
|
||||
remotePorts := c.RemotePorts
|
||||
if c.Remote || c.RemoteOnce {
|
||||
if len(remoteIPs) > 0 {
|
||||
dest.Address = net.IPAddress(randPrefix(remoteIPs[mrand.Intn(len(remoteIPs))]))
|
||||
}
|
||||
if len(remotePorts) > 0 {
|
||||
dest.Port = net.Port(remotePorts[mrand.Intn(len(remotePorts))])
|
||||
}
|
||||
}
|
||||
conn, err := dialer.DialUDP(*dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cur := conn.(*finalmask.PacketConnWrapper).PacketConn
|
||||
addr := conn.RemoteAddr().(*net.UDPAddr)
|
||||
client := &udpHopConn{
|
||||
dialer: dialer,
|
||||
local: c.Local,
|
||||
remote: c.Remote,
|
||||
conn := &udpHopConn{
|
||||
conn: raw,
|
||||
sockopt: c.Sockopt,
|
||||
local: c.Local,
|
||||
remote: c.Remote,
|
||||
remoteOnce: c.RemoteOnce,
|
||||
|
||||
intervalMin: c.IntervalMin,
|
||||
intervalMax: c.IntervalMax,
|
||||
remotePorts: c.RemotePorts,
|
||||
remoteIPs: remoteIPs,
|
||||
remotePorts: remotePorts,
|
||||
|
||||
cur: cur,
|
||||
addr: addr,
|
||||
readCh: make(chan packet),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
go client.run()
|
||||
client.wg.Add(1)
|
||||
go client.recv(client.cur)
|
||||
return client, nil
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) closed() bool {
|
||||
@@ -105,67 +93,61 @@ func (c *udpHopConn) closed() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpHopConn) run() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(crypto.RandBetween(c.intervalMin, c.intervalMax+1)))
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
ticker.Reset(time.Second * time.Duration(crypto.RandBetween(c.intervalMin, c.intervalMax+1)))
|
||||
c.hop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpHopConn) hop() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
func (c *udpHopConn) hop(addr *net.UDPAddr) {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
oldIP := c.addr.IP
|
||||
oldPort := c.addr.Port
|
||||
if c.remote {
|
||||
if len(c.remoteIPs) > 0 {
|
||||
c.addr.IP = randPrefix(c.remoteIPs[mrand.Intn(len(c.remoteIPs))])
|
||||
}
|
||||
newAddr := &net.UDPAddr{IP: addr.IP, Port: addr.Port}
|
||||
newConn := c.conn
|
||||
if c.remote || c.remoteOnce && c.addr == nil {
|
||||
if len(c.remotePorts) > 0 {
|
||||
c.addr.Port = int(c.remotePorts[mrand.Intn(len(c.remotePorts))])
|
||||
newAddr.Port = int(c.remotePorts[mrand.Intn(len(c.remotePorts))])
|
||||
}
|
||||
if len(c.remoteIPs) > 0 {
|
||||
newAddr.IP = randPrefix(c.remoteIPs[mrand.Intn(len(c.remoteIPs))])
|
||||
}
|
||||
}
|
||||
if c.local {
|
||||
conn, err := c.dialer.DialUDP(net.UDPDestination(net.IPAddress(c.addr.IP), net.Port(c.addr.Port)))
|
||||
raw, err := internet.DialSystem(context.Background(), net.UDPDestination(net.IPAddress(newAddr.IP), net.Port(newAddr.Port)), c.sockopt)
|
||||
if err != nil {
|
||||
c.addr.IP = oldIP
|
||||
c.addr.Port = oldPort
|
||||
errors.LogErrorInner(context.Background(), err, "hop err")
|
||||
return
|
||||
}
|
||||
conn.SetDeadline(c.deadline)
|
||||
conn.SetReadDeadline(c.readDeadline)
|
||||
conn.SetWriteDeadline(c.writeDeadline)
|
||||
switch c := raw.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
newConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
newConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
newConn.SetDeadline(c.deadline)
|
||||
newConn.SetReadDeadline(c.readDeadline)
|
||||
newConn.SetWriteDeadline(c.writeDeadline)
|
||||
if c.pre != nil {
|
||||
_ = c.pre.Close()
|
||||
}
|
||||
c.pre = c.cur
|
||||
c.cur = conn.(*finalmask.PacketConnWrapper).PacketConn
|
||||
c.wg.Add(1)
|
||||
go c.recv(c.cur)
|
||||
go c.recv(newConn)
|
||||
}
|
||||
c.addr = newAddr
|
||||
c.cur = newConn
|
||||
}
|
||||
|
||||
func (c *udpHopConn) recv(conn net.PacketConn) {
|
||||
defer c.wg.Done()
|
||||
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
p := pool.Get().([]byte)
|
||||
n, addr, err := conn.ReadFrom(p)
|
||||
if err != nil {
|
||||
pool.Put(p[:cap(p)])
|
||||
if c.closed() {
|
||||
return
|
||||
if goerrors.Is(err, io.EOF) || goerrors.Is(err, io.ErrClosedPipe) || goerrors.Is(err, gonet.ErrClosed) {
|
||||
break
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
@@ -174,10 +156,9 @@ func (c *udpHopConn) recv(conn net.PacketConn) {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err")
|
||||
return
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case c.readCh <- packet{p: p[:n], addr: addr}:
|
||||
@@ -188,6 +169,22 @@ func (c *udpHopConn) recv(conn net.PacketConn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpHopConn) hopLoop() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(crypto.RandBetween(c.intervalMin, c.intervalMax+1)))
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ticker.Reset(time.Second * time.Duration(crypto.RandBetween(c.intervalMin, c.intervalMax+1)))
|
||||
c.mu.Lock()
|
||||
c.hop(c.addr)
|
||||
c.mu.Unlock()
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpHopConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
packet, ok := <-c.readCh
|
||||
if ok {
|
||||
@@ -197,12 +194,21 @@ func (c *udpHopConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
}
|
||||
return n, packet.addr, packet.err
|
||||
}
|
||||
return 0, nil, io.ErrClosedPipe
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
|
||||
func (c *udpHopConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.cur == nil {
|
||||
c.hop(addr.(*net.UDPAddr))
|
||||
if c.cur == nil {
|
||||
return 0, nil
|
||||
}
|
||||
go c.hopLoop()
|
||||
}
|
||||
|
||||
_, err = c.cur.WriteTo(p, c.addr)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "send err")
|
||||
@@ -221,12 +227,15 @@ func (c *udpHopConn) Close() error {
|
||||
if c.pre != nil {
|
||||
_ = c.pre.Close()
|
||||
}
|
||||
_ = c.cur.Close()
|
||||
if c.cur != nil {
|
||||
_ = c.cur.Close()
|
||||
}
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case packet := <-c.readCh:
|
||||
if packet.p != nil {
|
||||
pool.Put(packet.p[:cap(packet.p)])
|
||||
case p := <-c.readCh:
|
||||
if p.p != nil {
|
||||
pool.Put(p.p[:cap(p.p)])
|
||||
}
|
||||
default:
|
||||
}
|
||||
@@ -235,9 +244,7 @@ func (c *udpHopConn) Close() error {
|
||||
}
|
||||
|
||||
func (c *udpHopConn) LocalAddr() net.Addr {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.cur.LocalAddr()
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *udpHopConn) SetDeadline(t time.Time) error {
|
||||
@@ -247,7 +254,10 @@ func (c *udpHopConn) SetDeadline(t time.Time) error {
|
||||
if c.pre != nil {
|
||||
_ = c.pre.SetDeadline(t)
|
||||
}
|
||||
return c.cur.SetDeadline(t)
|
||||
if c.cur != nil {
|
||||
_ = c.cur.SetDeadline(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) SetReadDeadline(t time.Time) error {
|
||||
@@ -257,7 +267,10 @@ func (c *udpHopConn) SetReadDeadline(t time.Time) error {
|
||||
if c.pre != nil {
|
||||
_ = c.pre.SetReadDeadline(t)
|
||||
}
|
||||
return c.cur.SetReadDeadline(t)
|
||||
if c.cur != nil {
|
||||
_ = c.cur.SetReadDeadline(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) SetWriteDeadline(t time.Time) error {
|
||||
@@ -267,7 +280,10 @@ func (c *udpHopConn) SetWriteDeadline(t time.Time) error {
|
||||
if c.pre != nil {
|
||||
_ = c.pre.SetWriteDeadline(t)
|
||||
}
|
||||
return c.cur.SetWriteDeadline(t)
|
||||
if c.cur != nil {
|
||||
_ = c.cur.SetWriteDeadline(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randPrefix(p netip.Prefix) []byte {
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package xdns
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return NewConnClient(c, conn)
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
// _, ok1 := raw.(*internet.FakePacketConn)
|
||||
// _, ok2 := raw.(*udphop.UdpHopPacketConn)
|
||||
// if level != 0 || ok1 || ok2 {
|
||||
// return nil, errors.New("xdns requires being at the outermost level")
|
||||
// }
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServer(c, conn)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
// if level != 0 {
|
||||
// return nil, errors.New("xdns requires being at the outermost level")
|
||||
// }
|
||||
return NewConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
mrand "math/rand"
|
||||
mathrand "math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
@@ -36,11 +36,11 @@ type packet struct {
|
||||
}
|
||||
|
||||
type xicmpConnClient struct {
|
||||
conn net.PacketConn
|
||||
icmp4 *icmp.PacketConn
|
||||
icmp6 *icmp.PacketConn
|
||||
udp bool
|
||||
ips []netip.Addr
|
||||
ip net.IP
|
||||
clientID [8]byte
|
||||
id int
|
||||
seq int
|
||||
@@ -50,7 +50,7 @@ type xicmpConnClient struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewConnClient(c *Config, dest *net.Destination) (net.PacketConn, error) {
|
||||
func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
var icmp4, icmp6 *icmp.PacketConn
|
||||
var err4, err6 error
|
||||
if c.DGRAM {
|
||||
@@ -69,24 +69,17 @@ func NewConnClient(c *Config, dest *net.Destination) (net.PacketConn, error) {
|
||||
ips = append(ips, netip.MustParseAddr(ip))
|
||||
}
|
||||
|
||||
var ip net.IP
|
||||
if len(ips) > 0 {
|
||||
ip = ips[mrand.Intn(len(ips))].AsSlice()
|
||||
} else {
|
||||
ip = dest.Address.IP()
|
||||
}
|
||||
|
||||
var clientID [8]byte
|
||||
common.Must2(rand.Read(clientID[:]))
|
||||
|
||||
conn := &xicmpConnClient{
|
||||
conn: raw,
|
||||
icmp4: icmp4,
|
||||
icmp6: icmp6,
|
||||
udp: c.DGRAM,
|
||||
ips: ips,
|
||||
ip: ip,
|
||||
clientID: clientID,
|
||||
id: mrand.Intn(65536),
|
||||
id: mathrand.Intn(65536),
|
||||
seq: 1,
|
||||
readCh: make(chan packet),
|
||||
closeCh: make(chan struct{}),
|
||||
@@ -99,6 +92,10 @@ func NewConnClient(c *Config, dest *net.Destination) (net.PacketConn, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) ring(a, b uint16) uint16 {
|
||||
return min(a-b, b-a)
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) closed() bool {
|
||||
select {
|
||||
case <-c.closeCh:
|
||||
@@ -113,11 +110,12 @@ func (c *xicmpConnClient) recv4() {
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
|
||||
n, addr, err := c.icmp4.ReadFrom(b[:])
|
||||
if err != nil {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
select {
|
||||
@@ -127,10 +125,9 @@ func (c *xicmpConnClient) recv4() {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err 4")
|
||||
return
|
||||
errors.LogErrorInner(context.Background(), err, "recv4 err")
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := icmp.ParseMessage(1, b[:n])
|
||||
@@ -153,6 +150,10 @@ func (c *xicmpConnClient) recv4() {
|
||||
continue
|
||||
}
|
||||
|
||||
if c.ring(uint16(echo.Seq), uint16(c.seq)) > 1000 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(echo.Data) > 8 && bytes.Equal(echo.Data[:8], c.clientID[:]) {
|
||||
continue
|
||||
}
|
||||
@@ -181,11 +182,12 @@ func (c *xicmpConnClient) recv6() {
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
|
||||
n, addr, err := c.icmp6.ReadFrom(b[:])
|
||||
if err != nil {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
select {
|
||||
@@ -195,10 +197,9 @@ func (c *xicmpConnClient) recv6() {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err 6")
|
||||
return
|
||||
errors.LogErrorInner(context.Background(), err, "recv6 err")
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := icmp.ParseMessage(58, b[:n])
|
||||
@@ -221,6 +222,10 @@ func (c *xicmpConnClient) recv6() {
|
||||
continue
|
||||
}
|
||||
|
||||
if c.ring(uint16(echo.Seq), uint16(c.seq)) > 1000 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(echo.Data) > 8 && bytes.Equal(echo.Data[:8], c.clientID[:]) {
|
||||
continue
|
||||
}
|
||||
@@ -268,9 +273,9 @@ func (c *xicmpConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
c.seq %= 65536
|
||||
c.mu.Unlock()
|
||||
|
||||
ip := c.ip
|
||||
ip := addr.(*net.UDPAddr).IP
|
||||
if len(c.ips) > 0 {
|
||||
ip = c.ips[mrand.Intn(len(c.ips))].AsSlice()
|
||||
ip = c.ips[mathrand.Intn(len(c.ips))].AsSlice()
|
||||
}
|
||||
|
||||
if c.udp {
|
||||
@@ -309,6 +314,7 @@ func (c *xicmpConnClient) Close() error {
|
||||
close(c.closeCh)
|
||||
_ = c.icmp4.Close()
|
||||
_ = c.icmp6.Close()
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case p := <-c.readCh:
|
||||
@@ -322,7 +328,7 @@ func (c *xicmpConnClient) Close() error {
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) LocalAddr() net.Addr {
|
||||
return &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) SetDeadline(t time.Time) error {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package xicmp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func (c *Config) HandleDial() {}
|
||||
|
||||
func (c *Config) HandleListen() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
if dest.Address.Family().IsDomain() && len(c.IPs) == 0 {
|
||||
return nil, errors.New("empty ip addresses")
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
_, ok1 := raw.(*internet.FakePacketConn)
|
||||
if level != 0 || ok1 {
|
||||
return nil, errors.New("xicmp requires being at the outermost level")
|
||||
}
|
||||
return NewConnClient(c, dest)
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
return NewConnServer(c)
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
if level != 0 {
|
||||
return nil, errors.New("xicmp requires being at the outermost level")
|
||||
}
|
||||
return NewConnServer(c, raw)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ type record struct {
|
||||
}
|
||||
|
||||
type xicmpConnServer struct {
|
||||
conn net.PacketConn
|
||||
icmp4 *icmp.PacketConn
|
||||
icmp6 *icmp.PacketConn
|
||||
ips map[netip.Addr]struct{}
|
||||
@@ -47,7 +48,7 @@ type xicmpConnServer struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewConnServer(c *Config) (net.PacketConn, error) {
|
||||
func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
icmp4, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -63,6 +64,7 @@ func NewConnServer(c *Config) (net.PacketConn, error) {
|
||||
}
|
||||
|
||||
conn := &xicmpConnServer{
|
||||
conn: raw,
|
||||
icmp4: icmp4,
|
||||
icmp6: icmp6,
|
||||
ips: ips,
|
||||
@@ -113,11 +115,12 @@ func (c *xicmpConnServer) recv4() {
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
|
||||
n, addr, err := c.icmp4.ReadFrom(b[:])
|
||||
if err != nil {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
select {
|
||||
@@ -127,10 +130,9 @@ func (c *xicmpConnServer) recv4() {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err 4")
|
||||
return
|
||||
errors.LogErrorInner(context.Background(), err, "recv4 err")
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := icmp.ParseMessage(1, b[:n])
|
||||
@@ -193,11 +195,12 @@ func (c *xicmpConnServer) recv6() {
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
|
||||
n, addr, err := c.icmp6.ReadFrom(b[:])
|
||||
if err != nil {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
select {
|
||||
@@ -207,10 +210,9 @@ func (c *xicmpConnServer) recv6() {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err 6")
|
||||
return
|
||||
errors.LogErrorInner(context.Background(), err, "recv6 err")
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := icmp.ParseMessage(58, b[:n])
|
||||
@@ -328,6 +330,7 @@ func (c *xicmpConnServer) Close() error {
|
||||
close(c.closeCh)
|
||||
_ = c.icmp4.Close()
|
||||
_ = c.icmp6.Close()
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case p := <-c.readCh:
|
||||
@@ -341,7 +344,7 @@ func (c *xicmpConnServer) Close() error {
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) LocalAddr() net.Addr {
|
||||
return &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) SetDeadline(t time.Time) error {
|
||||
|
||||
@@ -39,6 +39,7 @@ type record struct {
|
||||
}
|
||||
|
||||
type xicmpConnServer struct {
|
||||
conn net.PacketConn
|
||||
icmp4 *icmp.PacketConn
|
||||
icmp6 *icmp.PacketConn
|
||||
ipv4PC *ipv4.PacketConn
|
||||
@@ -51,7 +52,7 @@ type xicmpConnServer struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewConnServer(c *Config) (net.PacketConn, error) {
|
||||
func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
icmp4, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -67,6 +68,7 @@ func NewConnServer(c *Config) (net.PacketConn, error) {
|
||||
}
|
||||
|
||||
conn := &xicmpConnServer{
|
||||
conn: raw,
|
||||
icmp4: icmp4,
|
||||
icmp6: icmp6,
|
||||
ipv4PC: icmp4.IPv4PacketConn(),
|
||||
@@ -122,11 +124,12 @@ func (c *xicmpConnServer) recv4() {
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
|
||||
n, cm, addr, err := c.ipv4PC.ReadFrom(b[:])
|
||||
if err != nil {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
select {
|
||||
@@ -136,10 +139,9 @@ func (c *xicmpConnServer) recv4() {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err 4")
|
||||
return
|
||||
errors.LogErrorInner(context.Background(), err, "recv4 err")
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := icmp.ParseMessage(1, b[:n])
|
||||
@@ -203,11 +205,12 @@ func (c *xicmpConnServer) recv6() {
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
|
||||
n, cm, addr, err := c.ipv6PC.ReadFrom(b[:])
|
||||
if err != nil {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
select {
|
||||
@@ -217,10 +220,9 @@ func (c *xicmpConnServer) recv6() {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err 6")
|
||||
return
|
||||
errors.LogErrorInner(context.Background(), err, "recv6 err")
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := icmp.ParseMessage(58, b[:n])
|
||||
@@ -339,6 +341,7 @@ func (c *xicmpConnServer) Close() error {
|
||||
close(c.closeCh)
|
||||
_ = c.icmp4.Close()
|
||||
_ = c.icmp6.Close()
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case p := <-c.readCh:
|
||||
@@ -352,7 +355,7 @@ func (c *xicmpConnServer) Close() error {
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) LocalAddr() net.Addr {
|
||||
return &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) SetDeadline(t time.Time) error {
|
||||
|
||||
@@ -2,12 +2,10 @@ package xmc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
|
||||
func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
|
||||
@@ -83,6 +83,7 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
|
||||
}
|
||||
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
|
||||
realityConfig := reality.ConfigFromStreamSettings(streamSettings)
|
||||
sockopt := streamSettings.SocketSettings
|
||||
grpcSettings := streamSettings.ProtocolSettings.(*Config)
|
||||
|
||||
if client, found := globalDialerMap[dialerConf{dest, streamSettings}]; found && client.GetState() != connectivity.Shutdown {
|
||||
@@ -123,13 +124,17 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
|
||||
gctx = session.ContextWithOutbounds(gctx, session.OutboundsFromContext(ctx))
|
||||
gctx = session.ContextWithTimeoutOnly(gctx, true)
|
||||
|
||||
var c net.Conn
|
||||
if streamSettings.FinalMask != nil {
|
||||
c, err = streamSettings.FinalMask.DialTCP(gctx, net.TCPDestination(address, port))
|
||||
} else {
|
||||
c, err = internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
}
|
||||
c, err := internet.DialSystem(gctx, net.TCPDestination(address, port), sockopt)
|
||||
if err == nil {
|
||||
if streamSettings.TcpmaskManager != nil {
|
||||
newConn, err := streamSettings.TcpmaskManager.WrapConnClient(c)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, errors.New("mask err").Base(err)
|
||||
}
|
||||
c = newConn
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
config := tlsConfig.GetTLSConfig(tls.WithDestination(dest))
|
||||
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
|
||||
|
||||
@@ -104,20 +104,28 @@ func Listen(ctx context.Context, address net.Address, port net.Port, settings *i
|
||||
go func() {
|
||||
var streamListener net.Listener
|
||||
var err error
|
||||
var addr net.Addr
|
||||
if port == net.Port(0) { // unix
|
||||
addr = &net.UnixAddr{Name: address.Domain(), Net: "unix"}
|
||||
streamListener, err = internet.ListenSystem(ctx, &net.UnixAddr{
|
||||
Name: address.Domain(),
|
||||
Net: "unix",
|
||||
}, settings.SocketSettings)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(ctx, err, "failed to listen on ", address)
|
||||
return
|
||||
}
|
||||
} else { // tcp
|
||||
addr = &net.TCPAddr{IP: address.IP(), Port: int(port)}
|
||||
streamListener, err = internet.ListenSystem(ctx, &net.TCPAddr{
|
||||
IP: address.IP(),
|
||||
Port: int(port),
|
||||
}, settings.SocketSettings)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(ctx, err, "failed to listen on ", address, ":", port)
|
||||
return
|
||||
}
|
||||
}
|
||||
if settings.FinalMask != nil {
|
||||
streamListener, err = settings.FinalMask.Listen(ctx, addr)
|
||||
} else {
|
||||
streamListener, err = internet.ListenSystem(ctx, addr, settings.SocketSettings)
|
||||
}
|
||||
if err != nil {
|
||||
errors.LogErrorInner(ctx, err, "failed to listen on ", address, ":", port)
|
||||
return
|
||||
|
||||
if settings.TcpmaskManager != nil {
|
||||
streamListener, _ = settings.TcpmaskManager.WrapListener(streamListener)
|
||||
}
|
||||
|
||||
errors.LogDebug(ctx, "gRPC listen for service name `"+grpcSettings.getServiceName()+"` tun `"+grpcSettings.getTunStreamName()+"` multi tun `"+grpcSettings.getTunMultiStreamName()+"`")
|
||||
|
||||
@@ -46,18 +46,21 @@ func (c *ConnRF) Read(b []byte) (int, error) {
|
||||
func dialhttpUpgrade(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
|
||||
transportConfiguration := streamSettings.ProtocolSettings.(*Config)
|
||||
|
||||
var pconn net.Conn
|
||||
var err error
|
||||
if streamSettings.FinalMask != nil {
|
||||
pconn, err = streamSettings.FinalMask.DialTCP(ctx, dest)
|
||||
} else {
|
||||
pconn, err = internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
}
|
||||
pconn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(ctx, err, "failed to dial to ", dest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if streamSettings.TcpmaskManager != nil {
|
||||
newConn, err := streamSettings.TcpmaskManager.WrapConnClient(pconn)
|
||||
if err != nil {
|
||||
pconn.Close()
|
||||
return nil, errors.New("mask err").Base(err)
|
||||
}
|
||||
pconn = newConn
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
var requestURL url.URL
|
||||
tConfig := tls.ConfigFromStreamSettings(streamSettings)
|
||||
|
||||
@@ -124,21 +124,29 @@ func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port,
|
||||
}
|
||||
var listener net.Listener
|
||||
var err error
|
||||
var addr net.Addr
|
||||
if port == net.Port(0) { // unix
|
||||
addr = &net.UnixAddr{Name: address.Domain(), Net: "unix"}
|
||||
listener, err = internet.ListenSystem(ctx, &net.UnixAddr{
|
||||
Name: address.Domain(),
|
||||
Net: "unix",
|
||||
}, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to listen unix domain socket(for HttpUpgrade) on ", address).Base(err)
|
||||
}
|
||||
errors.LogInfo(ctx, "listening unix domain socket(for HttpUpgrade) on ", address)
|
||||
} else { // tcp
|
||||
addr = &net.TCPAddr{IP: address.IP(), Port: int(port)}
|
||||
listener, err = internet.ListenSystem(ctx, &net.TCPAddr{
|
||||
IP: address.IP(),
|
||||
Port: int(port),
|
||||
}, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to listen TCP(for HttpUpgrade) on ", address, ":", port).Base(err)
|
||||
}
|
||||
errors.LogInfo(ctx, "listening TCP(for HttpUpgrade) on ", address, ":", port)
|
||||
}
|
||||
if streamSettings.FinalMask != nil {
|
||||
listener, err = streamSettings.FinalMask.Listen(ctx, addr)
|
||||
} else {
|
||||
listener, err = internet.ListenSystem(ctx, addr, streamSettings.SocketSettings)
|
||||
|
||||
if streamSettings.TcpmaskManager != nil {
|
||||
listener, _ = streamSettings.TcpmaskManager.WrapListener(listener)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to listen ", addr.Network(), "(for HttpUpgrade) on ", address, ":", port).Base(err)
|
||||
}
|
||||
errors.LogInfo(ctx, "listening ", addr.Network(), "(for HttpUpgrade) on ", address, ":", port)
|
||||
|
||||
if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol {
|
||||
errors.LogWarning(ctx, "accepting PROXY protocol")
|
||||
|
||||
@@ -2,7 +2,7 @@ package hysteria
|
||||
|
||||
import (
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
go_tls "crypto/tls"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
@@ -28,12 +28,12 @@ import (
|
||||
type client struct {
|
||||
sync.Mutex
|
||||
|
||||
dest net.Destination
|
||||
config *Config
|
||||
tlsConfig *gotls.Config
|
||||
socketConfig *internet.SocketConfig
|
||||
finalMask *finalmask.FinalMask
|
||||
quicParams *internet.QuicParams
|
||||
dest net.Destination
|
||||
config *Config
|
||||
tlsConfig *go_tls.Config
|
||||
socketConfig *internet.SocketConfig
|
||||
udpmaskManager *finalmask.UdpmaskManager
|
||||
quicParams *internet.QuicParams
|
||||
|
||||
conn *quic.Conn
|
||||
tr *quic.Transport
|
||||
@@ -113,29 +113,30 @@ func (c *client) dial(ctx context.Context) error {
|
||||
// }
|
||||
|
||||
var pktConn net.PacketConn
|
||||
var udpAddr net.Addr
|
||||
if c.finalMask != nil {
|
||||
conn, err := c.finalMask.DialUDP(ctx, c.dest)
|
||||
var udpAddr *net.UDPAddr
|
||||
|
||||
raw, err := internet.DialSystem(ctx, c.dest, c.socketConfig)
|
||||
if err != nil {
|
||||
return errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
switch c := raw.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
udpAddr = raw.RemoteAddr().(*net.UDPAddr)
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
udpAddr = &net.UDPAddr{IP: c.RemoteAddr().(*net.TCPAddr).IP, Port: c.RemoteAddr().(*net.TCPAddr).Port}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
|
||||
if c.udpmaskManager != nil {
|
||||
newConn, err := c.udpmaskManager.WrapPacketConnClient(pktConn)
|
||||
if err != nil {
|
||||
return errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
pktConn = conn.(*finalmask.PacketConnWrapper).PacketConn
|
||||
udpAddr = conn.RemoteAddr()
|
||||
} else {
|
||||
conn, err := internet.DialSystem(ctx, c.dest, c.socketConfig)
|
||||
if err != nil {
|
||||
return errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
switch c := conn.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
udpAddr = c.RemoteAddr()
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
udpAddr = &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
pktConn.Close()
|
||||
return errors.New("mask err").Base(err)
|
||||
}
|
||||
pktConn = newConn
|
||||
}
|
||||
|
||||
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO}
|
||||
@@ -149,7 +150,7 @@ func (c *client) dial(ctx context.Context) error {
|
||||
rt := &http3.Transport{
|
||||
TLSClientConfig: c.tlsConfig,
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, _ string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
Dial: func(ctx context.Context, _ string, tlsCfg *go_tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
qc, err := tr.DialEarly(ctx, udpAddr, tlsCfg, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -315,12 +316,12 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
||||
c = manager.m[dialerConf{dest, streamSettings}]
|
||||
if c == nil {
|
||||
c = &client{
|
||||
dest: dest,
|
||||
config: streamSettings.ProtocolSettings.(*Config),
|
||||
tlsConfig: tlsConfig.GetTLSConfig(tls.WithDestination(dest)),
|
||||
socketConfig: streamSettings.SocketSettings,
|
||||
finalMask: streamSettings.FinalMask,
|
||||
quicParams: streamSettings.QuicParams,
|
||||
dest: dest,
|
||||
config: streamSettings.ProtocolSettings.(*Config),
|
||||
tlsConfig: tlsConfig.GetTLSConfig(tls.WithDestination(dest)),
|
||||
socketConfig: streamSettings.SocketSettings,
|
||||
udpmaskManager: streamSettings.UdpmaskManager,
|
||||
quicParams: streamSettings.QuicParams,
|
||||
}
|
||||
manager.m[dialerConf{dest, streamSettings}] = c
|
||||
}
|
||||
|
||||
@@ -316,17 +316,20 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
|
||||
quicConfig.MaxIncomingStreams = 1024
|
||||
}
|
||||
|
||||
var pktConn net.PacketConn
|
||||
var err error
|
||||
if streamSettings.FinalMask != nil {
|
||||
pktConn, err = streamSettings.FinalMask.ListenPacket(context.Background(), &net.UDPAddr{IP: address.IP(), Port: int(port)})
|
||||
} else {
|
||||
pktConn, err = internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: address.IP(), Port: int(port)}, streamSettings.SocketSettings)
|
||||
}
|
||||
pktConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: address.IP(), Port: int(port)}, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if streamSettings.UdpmaskManager != nil {
|
||||
newConn, err := streamSettings.UdpmaskManager.WrapPacketConnServer(pktConn)
|
||||
if err != nil {
|
||||
pktConn.Close()
|
||||
return nil, errors.New("mask err").Base(err)
|
||||
}
|
||||
pktConn = newConn
|
||||
}
|
||||
|
||||
var k *quic.StatelessResetKey
|
||||
if !quicParams.DisableStatelessReset {
|
||||
k = &quic.StatelessResetKey{}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
package hysteria
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/protocol/tls/cert"
|
||||
)
|
||||
|
||||
func TestDatagram(t *testing.T) {
|
||||
run := func() (addr net.Addr, recv chan int64, cancel func()) {
|
||||
cert, _ := cert.MustGenerate(nil)
|
||||
Certificate := [][]byte{cert.Certificate}
|
||||
PrivateKey := common.Must2(x509.ParsePKCS8PrivateKey(cert.PrivateKey))
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
{
|
||||
Certificate: Certificate,
|
||||
PrivateKey: PrivateKey,
|
||||
},
|
||||
},
|
||||
NextProtos: []string{"h3"},
|
||||
}
|
||||
|
||||
quicConf := &quic.Config{
|
||||
InitialStreamReceiveWindow: 8388608,
|
||||
MaxStreamReceiveWindow: 8388608,
|
||||
InitialConnectionReceiveWindow: 8388608 * 5 / 2,
|
||||
MaxConnectionReceiveWindow: 8388608 * 5 / 2,
|
||||
MaxIdleTimeout: 30 * time.Second,
|
||||
MaxIncomingStreams: 1024,
|
||||
DisablePathMTUDiscovery: runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin",
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: MaxDatagramFrameSize,
|
||||
AssumePeerMaxDatagramFrameSize: MaxDatagramFrameSize,
|
||||
DisablePathManager: true,
|
||||
}
|
||||
|
||||
pktConn := common.Must2(net.ListenPacket("udp", "127.0.0.1:0"))
|
||||
tr := &quic.Transport{Conn: pktConn}
|
||||
l := common.Must2(tr.Listen(tlsConf, quicConf))
|
||||
|
||||
recv = make(chan int64)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
defer pktConn.Close()
|
||||
defer tr.Close()
|
||||
defer l.Close()
|
||||
defer close(recv)
|
||||
|
||||
var buf [1500]byte
|
||||
for {
|
||||
conn, err := l.Accept(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Error(err)
|
||||
}
|
||||
break
|
||||
}
|
||||
err = conn.SendDatagram(buf[:])
|
||||
var qErr *quic.DatagramTooLargeError
|
||||
if !errors.As(err, &qErr) {
|
||||
t.Error(err)
|
||||
}
|
||||
recv <- qErr.MaxDatagramPayloadSize
|
||||
defer conn.CloseWithError(0, "")
|
||||
}
|
||||
}()
|
||||
|
||||
return l.Addr(), recv, cancel
|
||||
}
|
||||
|
||||
addr, recv, cancel := run()
|
||||
|
||||
t.Run("With ChromeParrot", func(t *testing.T) {
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
quicConf := &quic.Config{
|
||||
InitialStreamReceiveWindow: 8388608,
|
||||
MaxStreamReceiveWindow: 8388608,
|
||||
InitialConnectionReceiveWindow: 8388608 * 5 / 2,
|
||||
MaxConnectionReceiveWindow: 8388608 * 5 / 2,
|
||||
MaxIdleTimeout: 30 * time.Second,
|
||||
KeepAlivePeriod: 10 * time.Second,
|
||||
DisablePathMTUDiscovery: runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin",
|
||||
ChromeParrot: true,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: MaxDatagramFrameSize,
|
||||
OmitMaxDatagramFrameSize: true,
|
||||
DisablePathManager: true,
|
||||
}
|
||||
|
||||
pktConn := common.Must2(net.ListenPacket("udp", "127.0.0.1:0"))
|
||||
tr := &quic.Transport{Conn: pktConn, ConnectionIDGenerator: quic.ZeroLengthConnectionIDGenerator{}}
|
||||
conn := common.Must2(tr.DialEarly(context.Background(), addr, tlsConf, quicConf))
|
||||
|
||||
defer pktConn.Close()
|
||||
defer tr.Close()
|
||||
defer conn.CloseWithError(0, "")
|
||||
|
||||
var buf [1500]byte
|
||||
err := conn.SendDatagram(buf[:])
|
||||
var qErr *quic.DatagramTooLargeError
|
||||
if !errors.As(err, &qErr) || qErr.MaxDatagramPayloadSize != 1197 {
|
||||
t.Error(err)
|
||||
}
|
||||
if server := <-recv; server != 1243 {
|
||||
t.Error(server)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Without ChromeParrot", func(t *testing.T) {
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h3"},
|
||||
}
|
||||
|
||||
quicConf := &quic.Config{
|
||||
InitialStreamReceiveWindow: 8388608,
|
||||
MaxStreamReceiveWindow: 8388608,
|
||||
InitialConnectionReceiveWindow: 8388608 * 5 / 2,
|
||||
MaxConnectionReceiveWindow: 8388608 * 5 / 2,
|
||||
MaxIdleTimeout: 30 * time.Second,
|
||||
KeepAlivePeriod: 10 * time.Second,
|
||||
DisablePathMTUDiscovery: runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin",
|
||||
ChromeParrot: false,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: MaxDatagramFrameSize,
|
||||
OmitMaxDatagramFrameSize: true,
|
||||
DisablePathManager: true,
|
||||
}
|
||||
|
||||
pktConn := common.Must2(net.ListenPacket("udp", "127.0.0.1:0"))
|
||||
tr := &quic.Transport{Conn: pktConn}
|
||||
conn := common.Must2(tr.DialEarly(context.Background(), addr, tlsConf, quicConf))
|
||||
|
||||
defer pktConn.Close()
|
||||
defer tr.Close()
|
||||
defer conn.CloseWithError(0, "")
|
||||
|
||||
var buf [1500]byte
|
||||
err := conn.SendDatagram(buf[:])
|
||||
var qErr *quic.DatagramTooLargeError
|
||||
if !errors.As(err, &qErr) || qErr.MaxDatagramPayloadSize != 1197 {
|
||||
t.Error(err)
|
||||
}
|
||||
if server := <-recv; server != 1197 {
|
||||
t.Error(server)
|
||||
}
|
||||
})
|
||||
|
||||
cancel()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package kcp
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
reflect "reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/dice"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/net/cnc"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
@@ -49,17 +51,36 @@ func DialKCP(ctx context.Context, dest net.Destination, streamSettings *internet
|
||||
dest.Network = net.Network_UDP
|
||||
errors.LogInfo(ctx, "dialing mKCP to ", dest)
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if streamSettings.FinalMask != nil {
|
||||
conn, err = streamSettings.FinalMask.DialUDP(ctx, dest)
|
||||
} else {
|
||||
conn, err = internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
}
|
||||
conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest: ", err).AtWarning().Base(err)
|
||||
}
|
||||
|
||||
if streamSettings.UdpmaskManager != nil {
|
||||
var pktConn net.PacketConn
|
||||
var udpAddr *net.UDPAddr
|
||||
switch c := conn.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
udpAddr = c.RemoteAddr().(*net.UDPAddr)
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
udpAddr = &net.UDPAddr{IP: c.RemoteAddr().(*net.TCPAddr).IP, Port: c.RemoteAddr().(*net.TCPAddr).Port}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
newConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(pktConn)
|
||||
if err != nil {
|
||||
pktConn.Close()
|
||||
return nil, errors.New("mask err").Base(err)
|
||||
}
|
||||
pktConn = newConn
|
||||
conn = &internet.PacketConnWrapper{
|
||||
PacketConn: pktConn,
|
||||
Dest: udpAddr,
|
||||
}
|
||||
}
|
||||
|
||||
kcpSettings := streamSettings.ProtocolSettings.(*Config)
|
||||
|
||||
reader := &KCPPacketReader{}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
const protocolName = "masque"
|
||||
|
||||
const DefaultPath = "/.well-known/masque/ip/*/*/"
|
||||
|
||||
func init() {
|
||||
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
|
||||
return &Config{
|
||||
Path: DefaultPath,
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: transport/internet/masque/config.proto
|
||||
|
||||
package masque
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
||||
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
|
||||
Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_masque_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Config) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_masque_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_masque_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetHost() string {
|
||||
if x != nil {
|
||||
return x.Host
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetPath() string {
|
||||
if x != nil {
|
||||
return x.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetHeaders() map[string]string {
|
||||
if x != nil {
|
||||
return x.Headers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_masque_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_masque_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"&transport/internet/masque/config.proto\x12\x1exray.transport.internet.masque\"\xbb\x01\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" +
|
||||
"\x04path\x18\x02 \x01(\tR\x04path\x12M\n" +
|
||||
"\aheaders\x18\x03 \x03(\v23.xray.transport.internet.masque.Config.HeadersEntryR\aheaders\x1a:\n" +
|
||||
"\fHeadersEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B|\n" +
|
||||
"\"com.xray.transport.internet.masqueP\x01Z3github.com/xtls/xray-core/transport/internet/masque\xaa\x02\x1eXray.Transport.Internet.Masqueb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_masque_config_proto_rawDescOnce sync.Once
|
||||
file_transport_internet_masque_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_transport_internet_masque_config_proto_rawDescGZIP() []byte {
|
||||
file_transport_internet_masque_config_proto_rawDescOnce.Do(func() {
|
||||
file_transport_internet_masque_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_masque_config_proto_rawDesc), len(file_transport_internet_masque_config_proto_rawDesc)))
|
||||
})
|
||||
return file_transport_internet_masque_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_masque_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_transport_internet_masque_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.masque.Config
|
||||
nil, // 1: xray.transport.internet.masque.Config.HeadersEntry
|
||||
}
|
||||
var file_transport_internet_masque_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.transport.internet.masque.Config.headers:type_name -> xray.transport.internet.masque.Config.HeadersEntry
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_masque_config_proto_init() }
|
||||
func file_transport_internet_masque_config_proto_init() {
|
||||
if File_transport_internet_masque_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_masque_config_proto_rawDesc), len(file_transport_internet_masque_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_masque_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_masque_config_proto_depIdxs,
|
||||
MessageInfos: file_transport_internet_masque_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_masque_config_proto = out.File
|
||||
file_transport_internet_masque_config_proto_goTypes = nil
|
||||
file_transport_internet_masque_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.transport.internet.masque;
|
||||
option csharp_namespace = "Xray.Transport.Internet.Masque";
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/masque";
|
||||
option java_package = "com.xray.transport.internet.masque";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Config {
|
||||
string host = 1;
|
||||
string path = 2;
|
||||
map<string, string> headers = 3;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/masque/connectip"
|
||||
)
|
||||
|
||||
type PacketTooBigError struct {
|
||||
ICMP []byte
|
||||
}
|
||||
|
||||
func (e *PacketTooBigError) Error() string {
|
||||
return "packet too big for the tunnel"
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
ipConn *connectip.Conn
|
||||
quicConn *quic.Conn
|
||||
local []netip.Addr
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddrs() []netip.Addr {
|
||||
return c.local
|
||||
}
|
||||
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
return c.ipConn.ReadPacket(b)
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (int, error) {
|
||||
icmp, err := c.ipConn.WritePacket(b)
|
||||
if err != nil {
|
||||
if go_errors.Is(err, connectip.ErrMTUTooSmall) {
|
||||
errors.LogWarning(context.Background(), "MASQUE: closing the tunnel as it cannot carry ", MinPacketSize, "-byte packets")
|
||||
} else {
|
||||
errors.LogInfoInner(context.Background(), err, "MASQUE: closing the tunnel as sending failed")
|
||||
}
|
||||
c.Close()
|
||||
return 0, err
|
||||
}
|
||||
if len(icmp) > 0 {
|
||||
return 0, &PacketTooBigError{ICMP: icmp}
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
c.ipConn.Close()
|
||||
c.quicConn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr {
|
||||
return c.quicConn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.quicConn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) SetDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetReadDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) serveAddressAssignments() {
|
||||
for {
|
||||
assigned, err := c.ipConn.ReceiveAddressAssignment(context.Background())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, addr := range c.local {
|
||||
if !slices.ContainsFunc(assigned, func(a connectip.AssignedAddress) bool { return !a.Rejected() && a.IPPrefix.Contains(addr) }) {
|
||||
errors.LogInfo(context.Background(), "MASQUE: closing the tunnel as the proxy withdrew ", addr)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(localAddrs(assigned)) > len(c.local) {
|
||||
errors.LogInfo(context.Background(), "MASQUE: the proxy assigned another IP family, which is used once the tunnel is set up again")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) serveAddressRequests() {
|
||||
for {
|
||||
req, err := c.ipConn.ReceiveAddressRequest(context.Background())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := req.Respond(make([]netip.Prefix, len(req.Prefixes)), nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
Copyright 2024 Marten Seemann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,86 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var (
|
||||
rejectedIPv4Prefix = netip.PrefixFrom(netip.IPv4Unspecified(), 32)
|
||||
rejectedIPv6Prefix = netip.PrefixFrom(netip.IPv6Unspecified(), 128)
|
||||
)
|
||||
|
||||
type AddressRequestID uint64
|
||||
|
||||
type AddressRequest struct {
|
||||
Prefixes []netip.Prefix
|
||||
|
||||
conn *Conn
|
||||
requested *addressRequestCapsule
|
||||
responded *atomic.Bool
|
||||
}
|
||||
|
||||
func newAddressRequest(conn *Conn, requested *addressRequestCapsule) *AddressRequest {
|
||||
return &AddressRequest{
|
||||
Prefixes: slices.Clone(requested.Prefixes),
|
||||
conn: conn,
|
||||
requested: requested,
|
||||
responded: &atomic.Bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AddressRequest) Respond(assignments, additional []netip.Prefix) error {
|
||||
if r.conn == nil {
|
||||
return errors.New("connect-ip: invalid address request")
|
||||
}
|
||||
if len(assignments) != len(r.requested.RequestIDs) {
|
||||
return fmt.Errorf(
|
||||
"connect-ip: expected %d address assignments, got %d",
|
||||
len(r.requested.RequestIDs),
|
||||
len(assignments),
|
||||
)
|
||||
}
|
||||
capsule := &addressAssignCapsule{
|
||||
AssignedAddresses: make([]AssignedAddress, 0, len(assignments)+len(additional)),
|
||||
}
|
||||
var zeroPrefix netip.Prefix
|
||||
for i, p := range assignments {
|
||||
if p == zeroPrefix {
|
||||
if r.requested.Prefixes[i].Addr().Is4() {
|
||||
p = rejectedIPv4Prefix
|
||||
} else {
|
||||
p = rejectedIPv6Prefix
|
||||
}
|
||||
} else if !p.IsValid() || p != p.Masked() {
|
||||
return fmt.Errorf("connect-ip: invalid assigned prefix %d: %s", i, p)
|
||||
}
|
||||
capsule.AssignedAddresses = append(
|
||||
capsule.AssignedAddresses,
|
||||
AssignedAddress{RequestID: r.requested.RequestIDs[i], IPPrefix: p},
|
||||
)
|
||||
}
|
||||
for i, p := range additional {
|
||||
if !p.IsValid() || p != p.Masked() {
|
||||
return fmt.Errorf("connect-ip: invalid additional prefix %d: %s", i, p)
|
||||
}
|
||||
capsule.AssignedAddresses = append(capsule.AssignedAddresses, AssignedAddress{IPPrefix: p})
|
||||
}
|
||||
if !r.responded.CompareAndSwap(false, true) {
|
||||
return errors.New("connect-ip: address request already answered")
|
||||
}
|
||||
restrictPeer := slices.ContainsFunc(capsule.AssignedAddresses, func(a AssignedAddress) bool { return !a.Rejected() })
|
||||
if err := r.conn.sendAddressAssignment(capsule, restrictPeer); err != nil {
|
||||
r.responded.Store(false)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAddressRequests(t *testing.T) {
|
||||
client, server := setupConns(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
prefixes := []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/32"),
|
||||
netip.MustParsePrefix("0.0.0.0/32"),
|
||||
netip.MustParsePrefix("::/64"),
|
||||
}
|
||||
ids, err := client.RequestAddresses(prefixes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []AddressRequestID{1, 2, 3}, ids)
|
||||
req, err := server.ReceiveAddressRequest(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, prefixes, req.Prefixes)
|
||||
|
||||
assignments := []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32"), {}, {}}
|
||||
additional := []netip.Prefix{netip.MustParsePrefix("2001:db8::/64")}
|
||||
require.NoError(t, req.Respond(assignments, additional))
|
||||
received, err := client.ReceiveAddressAssignment(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, received, 4)
|
||||
require.Equal(t, AssignedAddress{RequestID: ids[0], IPPrefix: assignments[0]}, received[0])
|
||||
require.Equal(t, ids[1], received[1].RequestID)
|
||||
require.True(t, received[1].Rejected())
|
||||
require.Equal(t, ids[2], received[2].RequestID)
|
||||
require.True(t, received[2].Rejected())
|
||||
require.Equal(t, AssignedAddress{IPPrefix: additional[0]}, received[3])
|
||||
|
||||
ids, err = client.RequestAddresses(prefixes[:1])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []AddressRequestID{4}, ids)
|
||||
}
|
||||
|
||||
func TestAddressRequestValidation(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
defer conn.Close()
|
||||
|
||||
for _, prefixes := range [][]netip.Prefix{
|
||||
nil,
|
||||
{{}},
|
||||
{netip.MustParsePrefix("192.0.2.1/24")},
|
||||
{netip.MustParsePrefix("2001:db8::1/64")},
|
||||
} {
|
||||
ids, err := conn.RequestAddresses(prefixes)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ids)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressResponseValidation(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
defer conn.Close()
|
||||
|
||||
prefixes := []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}
|
||||
req := newAddressRequest(conn, &addressRequestCapsule{RequestIDs: []AddressRequestID{1}, Prefixes: prefixes})
|
||||
require.ErrorContains(t, (&AddressRequest{}).Respond(nil, nil), "invalid address request")
|
||||
require.ErrorContains(t, req.Respond(nil, nil), "expected 1 address assignments")
|
||||
require.ErrorContains(t, req.Respond(prefixes, []netip.Prefix{{}}), "invalid additional prefix")
|
||||
require.ErrorContains(t,
|
||||
req.Respond([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")}, nil),
|
||||
"invalid assigned prefix",
|
||||
)
|
||||
|
||||
copied := *req
|
||||
require.NoError(t, req.Respond(prefixes, nil))
|
||||
require.ErrorContains(t, copied.Respond(prefixes, nil), "already answered")
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/apernet/quic-go/quicvarint"
|
||||
)
|
||||
|
||||
const (
|
||||
capsuleTypeDatagram http3.CapsuleType = 0
|
||||
capsuleTypeAddressAssign http3.CapsuleType = 1
|
||||
capsuleTypeAddressRequest http3.CapsuleType = 2
|
||||
capsuleTypeRouteAdvertisement http3.CapsuleType = 3
|
||||
)
|
||||
|
||||
const (
|
||||
maxAddressesPerCapsule = 8192
|
||||
maxRoutesPerCapsule = 8192
|
||||
)
|
||||
|
||||
type addressAssignCapsule struct {
|
||||
AssignedAddresses []AssignedAddress
|
||||
}
|
||||
|
||||
type AssignedAddress struct {
|
||||
RequestID AddressRequestID
|
||||
IPPrefix netip.Prefix
|
||||
}
|
||||
|
||||
func (a AssignedAddress) Rejected() bool {
|
||||
return a.IPPrefix == rejectedIPv4Prefix || a.IPPrefix == rejectedIPv6Prefix
|
||||
}
|
||||
|
||||
func (a AssignedAddress) len() int {
|
||||
return quicvarint.Len(uint64(a.RequestID)) + 1 + a.IPPrefix.Addr().BitLen()/8 + 1
|
||||
}
|
||||
|
||||
type addressRequestCapsule struct {
|
||||
RequestIDs []AddressRequestID
|
||||
Prefixes []netip.Prefix
|
||||
}
|
||||
|
||||
func parseAddressAssignCapsule(r http3.CapsuleReader) (*addressAssignCapsule, error) {
|
||||
var assignedAddresses []AssignedAddress
|
||||
for r.Remaining() > 0 {
|
||||
if len(assignedAddresses) >= maxAddressesPerCapsule {
|
||||
return nil, fmt.Errorf("%w: ADDRESS_ASSIGN capsule contains too many addresses (maximum %d)", errCapsuleLimit, maxAddressesPerCapsule)
|
||||
}
|
||||
requestID, prefix, err := parseAddress(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assignedAddresses = append(assignedAddresses, AssignedAddress{RequestID: AddressRequestID(requestID), IPPrefix: prefix})
|
||||
}
|
||||
return &addressAssignCapsule{AssignedAddresses: assignedAddresses}, nil
|
||||
}
|
||||
|
||||
func (c *addressAssignCapsule) append(b []byte) []byte {
|
||||
totalLen := 0
|
||||
for _, addr := range c.AssignedAddresses {
|
||||
totalLen += addr.len()
|
||||
}
|
||||
|
||||
b = quicvarint.Append(b, uint64(capsuleTypeAddressAssign))
|
||||
b = quicvarint.Append(b, uint64(totalLen))
|
||||
|
||||
for _, addr := range c.AssignedAddresses {
|
||||
b = quicvarint.Append(b, uint64(addr.RequestID))
|
||||
if addr.IPPrefix.Addr().Is4() {
|
||||
b = append(b, 4)
|
||||
} else {
|
||||
b = append(b, 6)
|
||||
}
|
||||
b = append(b, addr.IPPrefix.Addr().AsSlice()...)
|
||||
b = append(b, byte(addr.IPPrefix.Bits()))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func parseAddressRequestCapsule(r http3.CapsuleReader) (*addressRequestCapsule, error) {
|
||||
if r.Remaining() == 0 {
|
||||
return nil, errors.New("ADDRESS_REQUEST capsule contains no addresses")
|
||||
}
|
||||
capsule := &addressRequestCapsule{}
|
||||
for r.Remaining() > 0 {
|
||||
if len(capsule.Prefixes) >= maxAddressesPerCapsule {
|
||||
return nil, fmt.Errorf("%w: ADDRESS_REQUEST capsule contains too many addresses (maximum %d)", errCapsuleLimit, maxAddressesPerCapsule)
|
||||
}
|
||||
requestID, prefix, err := parseAddress(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if requestID == 0 {
|
||||
return nil, errors.New("ADDRESS_REQUEST capsule contains a zero request ID")
|
||||
}
|
||||
capsule.RequestIDs = append(capsule.RequestIDs, AddressRequestID(requestID))
|
||||
capsule.Prefixes = append(capsule.Prefixes, prefix)
|
||||
}
|
||||
return capsule, nil
|
||||
}
|
||||
|
||||
func (c *addressRequestCapsule) append(b []byte) []byte {
|
||||
var totalLen int
|
||||
for i, p := range c.Prefixes {
|
||||
totalLen += quicvarint.Len(uint64(c.RequestIDs[i])) + 1 + p.Addr().BitLen()/8 + 1
|
||||
}
|
||||
|
||||
b = quicvarint.Append(b, uint64(capsuleTypeAddressRequest))
|
||||
b = quicvarint.Append(b, uint64(totalLen))
|
||||
|
||||
for i, p := range c.Prefixes {
|
||||
b = quicvarint.Append(b, uint64(c.RequestIDs[i]))
|
||||
if p.Addr().Is4() {
|
||||
b = append(b, 4)
|
||||
} else {
|
||||
b = append(b, 6)
|
||||
}
|
||||
b = append(b, p.Addr().AsSlice()...)
|
||||
b = append(b, byte(p.Bits()))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func parseAddress(r io.Reader) (requestID uint64, prefix netip.Prefix, _ error) {
|
||||
vr := quicvarint.NewReader(r)
|
||||
requestID, err := quicvarint.Read(vr)
|
||||
if err != nil {
|
||||
return 0, netip.Prefix{}, err
|
||||
}
|
||||
ipVersion, err := vr.ReadByte()
|
||||
if err != nil {
|
||||
return 0, netip.Prefix{}, err
|
||||
}
|
||||
var ip netip.Addr
|
||||
switch ipVersion {
|
||||
case 4:
|
||||
var ipv4 [4]byte
|
||||
if _, err := io.ReadFull(r, ipv4[:]); err != nil {
|
||||
return 0, netip.Prefix{}, err
|
||||
}
|
||||
ip = netip.AddrFrom4(ipv4)
|
||||
case 6:
|
||||
var ipv6 [16]byte
|
||||
if _, err := io.ReadFull(r, ipv6[:]); err != nil {
|
||||
return 0, netip.Prefix{}, err
|
||||
}
|
||||
ip = netip.AddrFrom16(ipv6)
|
||||
default:
|
||||
return 0, netip.Prefix{}, fmt.Errorf("invalid IP version: %d", ipVersion)
|
||||
}
|
||||
prefixLen, err := vr.ReadByte()
|
||||
if err != nil {
|
||||
return 0, netip.Prefix{}, err
|
||||
}
|
||||
if int(prefixLen) > ip.BitLen() {
|
||||
return 0, netip.Prefix{}, fmt.Errorf("prefix length %d exceeds IP address length (%d)", prefixLen, ip.BitLen())
|
||||
}
|
||||
prefix = netip.PrefixFrom(ip, int(prefixLen))
|
||||
if prefix != prefix.Masked() {
|
||||
return 0, netip.Prefix{}, errors.New("lower bits not covered by prefix length are not all zero")
|
||||
}
|
||||
return requestID, prefix, nil
|
||||
}
|
||||
|
||||
type routeAdvertisementCapsule struct {
|
||||
IPAddressRanges []IPRoute
|
||||
}
|
||||
|
||||
type IPRoute struct {
|
||||
StartIP netip.Addr
|
||||
EndIP netip.Addr
|
||||
IPProtocol uint8
|
||||
}
|
||||
|
||||
func (r IPRoute) len() int { return 1 + r.StartIP.BitLen()/8 + r.EndIP.BitLen()/8 + 1 }
|
||||
|
||||
func (r IPRoute) Prefixes() []netip.Prefix { return rangeToPrefixes(r.StartIP, r.EndIP) }
|
||||
|
||||
func parseRouteAdvertisementCapsule(r http3.CapsuleReader) (*routeAdvertisementCapsule, error) {
|
||||
var ranges []IPRoute
|
||||
for r.Remaining() > 0 {
|
||||
if len(ranges) >= maxRoutesPerCapsule {
|
||||
return nil, fmt.Errorf("%w: ROUTE_ADVERTISEMENT capsule contains too many routes (maximum %d)", errCapsuleLimit, maxRoutesPerCapsule)
|
||||
}
|
||||
ipRange, err := parseIPAddressRange(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ranges) > 0 {
|
||||
if err := checkRouteOrder(ranges[len(ranges)-1], ipRange); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ranges = append(ranges, ipRange)
|
||||
}
|
||||
return &routeAdvertisementCapsule{IPAddressRanges: ranges}, nil
|
||||
}
|
||||
|
||||
func (r IPRoute) validate() error {
|
||||
if !r.StartIP.IsValid() || !r.EndIP.IsValid() || r.StartIP.Zone() != "" || r.EndIP.Zone() != "" {
|
||||
return fmt.Errorf("invalid IP address range %s-%s", r.StartIP, r.EndIP)
|
||||
}
|
||||
if r.StartIP.Is4() != r.EndIP.Is4() {
|
||||
return fmt.Errorf("IP address range %s-%s mixes IP versions", r.StartIP, r.EndIP)
|
||||
}
|
||||
if r.StartIP.Compare(r.EndIP) > 0 {
|
||||
return fmt.Errorf("start IP %s is greater than end IP %s", r.StartIP, r.EndIP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkRouteOrder(a, b IPRoute) error {
|
||||
switch cmp.Or(
|
||||
cmp.Compare(a.StartIP.BitLen(), b.StartIP.BitLen()),
|
||||
cmp.Compare(a.IPProtocol, b.IPProtocol),
|
||||
) {
|
||||
case 1:
|
||||
return fmt.Errorf("routes are not ordered by IP version and IP protocol: %s-%s (protocol %d) precedes %s-%s (protocol %d)",
|
||||
a.StartIP, a.EndIP, a.IPProtocol, b.StartIP, b.EndIP, b.IPProtocol)
|
||||
case 0:
|
||||
if a.EndIP.Compare(b.StartIP) >= 0 {
|
||||
return fmt.Errorf("IP address ranges %s-%s and %s-%s (protocol %d) overlap or are not in ascending order",
|
||||
a.StartIP, a.EndIP, b.StartIP, b.EndIP, b.IPProtocol)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *routeAdvertisementCapsule) append(b []byte) []byte {
|
||||
var totalLen int
|
||||
for _, ipRange := range c.IPAddressRanges {
|
||||
totalLen += ipRange.len()
|
||||
}
|
||||
|
||||
b = quicvarint.Append(b, uint64(capsuleTypeRouteAdvertisement))
|
||||
b = quicvarint.Append(b, uint64(totalLen))
|
||||
|
||||
for _, ipRange := range c.IPAddressRanges {
|
||||
if ipRange.StartIP.Is4() {
|
||||
b = append(b, 4)
|
||||
} else {
|
||||
b = append(b, 6)
|
||||
}
|
||||
b = append(b, ipRange.StartIP.AsSlice()...)
|
||||
b = append(b, ipRange.EndIP.AsSlice()...)
|
||||
b = append(b, ipRange.IPProtocol)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func parseIPAddressRange(r io.Reader) (IPRoute, error) {
|
||||
var ipVersion uint8
|
||||
if err := binary.Read(r, binary.LittleEndian, &ipVersion); err != nil {
|
||||
return IPRoute{}, err
|
||||
}
|
||||
|
||||
var startIP, endIP netip.Addr
|
||||
switch ipVersion {
|
||||
case 4:
|
||||
var start, end [4]byte
|
||||
if _, err := io.ReadFull(r, start[:]); err != nil {
|
||||
return IPRoute{}, err
|
||||
}
|
||||
if _, err := io.ReadFull(r, end[:]); err != nil {
|
||||
return IPRoute{}, err
|
||||
}
|
||||
startIP = netip.AddrFrom4(start)
|
||||
endIP = netip.AddrFrom4(end)
|
||||
case 6:
|
||||
var start, end [16]byte
|
||||
if _, err := io.ReadFull(r, start[:]); err != nil {
|
||||
return IPRoute{}, err
|
||||
}
|
||||
if _, err := io.ReadFull(r, end[:]); err != nil {
|
||||
return IPRoute{}, err
|
||||
}
|
||||
startIP = netip.AddrFrom16(start)
|
||||
endIP = netip.AddrFrom16(end)
|
||||
default:
|
||||
return IPRoute{}, fmt.Errorf("invalid IP version: %d", ipVersion)
|
||||
}
|
||||
|
||||
if startIP.Compare(endIP) > 0 {
|
||||
return IPRoute{}, errors.New("start IP is greater than end IP")
|
||||
}
|
||||
|
||||
var ipProtocol uint8
|
||||
if err := binary.Read(r, binary.LittleEndian, &ipProtocol); err != nil {
|
||||
return IPRoute{}, err
|
||||
}
|
||||
return IPRoute{
|
||||
StartIP: startIP,
|
||||
EndIP: endIP,
|
||||
IPProtocol: ipProtocol,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/apernet/quic-go/quicvarint"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newCapsuleReader(t *testing.T, typ http3.CapsuleType, payload []byte) http3.CapsuleReader {
|
||||
t.Helper()
|
||||
|
||||
data := quicvarint.Append(nil, uint64(typ))
|
||||
data = quicvarint.Append(data, uint64(len(payload)))
|
||||
data = append(data, payload...)
|
||||
parsedType, cr, err := http3.NewCapsuleParser(bytes.NewReader(data)).Next()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, typ, parsedType)
|
||||
return cr
|
||||
}
|
||||
|
||||
func testIncompleteCapsule(t *testing.T, data []byte, parse func(http3.CapsuleReader) error) {
|
||||
t.Helper()
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
_, cr, err := http3.NewCapsuleParser(r).Next()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, parse(cr))
|
||||
require.Zero(t, r.Len())
|
||||
for i := range data {
|
||||
_, cr, err := http3.NewCapsuleParser(bytes.NewReader(data[:i])).Next()
|
||||
if err != nil {
|
||||
if i == 0 {
|
||||
require.ErrorIs(t, err, io.EOF)
|
||||
} else {
|
||||
require.ErrorIs(t, err, io.ErrUnexpectedEOF)
|
||||
}
|
||||
continue
|
||||
}
|
||||
require.ErrorIs(t, parse(cr), io.ErrUnexpectedEOF)
|
||||
}
|
||||
}
|
||||
|
||||
func testCapsuleEntryLimit[T any](t *testing.T, typ http3.CapsuleType, limit int, entry func(i int) []byte, parse func(http3.CapsuleReader) (*T, error)) {
|
||||
t.Helper()
|
||||
var payload []byte
|
||||
for i := range limit {
|
||||
payload = append(payload, entry(i)...)
|
||||
}
|
||||
r := newCapsuleReader(t, typ, payload)
|
||||
_, err := parse(r)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, r.Remaining())
|
||||
|
||||
data := quicvarint.Append(nil, uint64(typ))
|
||||
data = quicvarint.Append(data, uint64(len(payload)+1))
|
||||
_, r, err = http3.NewCapsuleParser(bytes.NewReader(append(data, payload...))).Next()
|
||||
require.NoError(t, err)
|
||||
_, err = parse(r)
|
||||
require.ErrorContains(t, err, "too many")
|
||||
require.Equal(t, int64(1), r.Remaining())
|
||||
}
|
||||
|
||||
func TestParseAddressAssignCapsule(t *testing.T) {
|
||||
addr1 := quicvarint.Append(nil, 1337)
|
||||
addr1 = append(addr1, 4)
|
||||
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 0}).AsSlice()...)
|
||||
addr1 = append(addr1, 24)
|
||||
addr2 := quicvarint.Append(nil, 1338)
|
||||
addr2 = append(addr2, 6)
|
||||
addr2 = append(addr2, netip.MustParseAddr("2001:db8::1").AsSlice()...)
|
||||
addr2 = append(addr2, 128)
|
||||
|
||||
data := quicvarint.Append(nil, uint64(capsuleTypeAddressAssign))
|
||||
data = quicvarint.Append(data, uint64(len(addr1)+len(addr2)))
|
||||
data = append(data, addr1...)
|
||||
data = append(data, addr2...)
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
typ, cr, err := http3.NewCapsuleParser(r).Next()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, capsuleTypeAddressAssign, typ)
|
||||
capsule, err := parseAddressAssignCapsule(cr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
[]AssignedAddress{
|
||||
{RequestID: 1337, IPPrefix: netip.MustParsePrefix("1.2.3.0/24")},
|
||||
{RequestID: 1338, IPPrefix: netip.MustParsePrefix("2001:db8::1/128")},
|
||||
},
|
||||
capsule.AssignedAddresses,
|
||||
)
|
||||
require.Zero(t, r.Len())
|
||||
}
|
||||
|
||||
func TestParseAddressAssignCapsuleLimit(t *testing.T) {
|
||||
entry := []byte{1, 4, 192, 0, 2, 1, 32}
|
||||
testCapsuleEntryLimit(t, capsuleTypeAddressAssign, maxAddressesPerCapsule, func(int) []byte { return entry }, parseAddressAssignCapsule)
|
||||
}
|
||||
|
||||
func TestAssignedAddressRejected(t *testing.T) {
|
||||
for _, prefix := range []string{"0.0.0.0/32", "::/128"} {
|
||||
require.True(t, (AssignedAddress{RequestID: 1, IPPrefix: netip.MustParsePrefix(prefix)}).Rejected())
|
||||
}
|
||||
for _, prefix := range []string{"0.0.0.0/0", "0.0.0.0/31", "::/0", "::/127", "192.0.2.1/32", "2001:db8::1/128"} {
|
||||
require.False(t, (AssignedAddress{RequestID: 1, IPPrefix: netip.MustParsePrefix(prefix)}).Rejected())
|
||||
}
|
||||
require.False(t, (AssignedAddress{}).Rejected())
|
||||
}
|
||||
|
||||
func TestWriteAddressAssignCapsule(t *testing.T) {
|
||||
c := &addressAssignCapsule{
|
||||
AssignedAddresses: []AssignedAddress{
|
||||
{RequestID: 1337, IPPrefix: netip.MustParsePrefix("1.2.3.0/24")},
|
||||
{RequestID: 1338, IPPrefix: netip.MustParsePrefix("2001:db8::1/128")},
|
||||
},
|
||||
}
|
||||
data := c.append(nil)
|
||||
r := bytes.NewReader(data)
|
||||
typ, cr, err := http3.NewCapsuleParser(r).Next()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, capsuleTypeAddressAssign, typ)
|
||||
parsed, err := parseAddressAssignCapsule(cr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c, parsed)
|
||||
require.Zero(t, r.Len())
|
||||
}
|
||||
|
||||
func TestParseAddressAssignCapsuleInvalid(t *testing.T) {
|
||||
testParseAddressCapsuleInvalid(t, capsuleTypeAddressAssign, func(r http3.CapsuleReader) error {
|
||||
_, err := parseAddressAssignCapsule(r)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func testParseAddressCapsuleInvalid(t *testing.T, typ http3.CapsuleType, f func(r http3.CapsuleReader) error) {
|
||||
t.Run("invalid IP version", func(t *testing.T) {
|
||||
addr1 := quicvarint.Append(nil, 1337)
|
||||
addr1 = append(addr1, 5)
|
||||
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
|
||||
addr1 = append(addr1, 32)
|
||||
require.ErrorContains(t, f(newCapsuleReader(t, typ, addr1)), "invalid IP version: 5")
|
||||
})
|
||||
|
||||
t.Run("invalid prefix length", func(t *testing.T) {
|
||||
addr1 := quicvarint.Append(nil, 1337)
|
||||
addr1 = append(addr1, 4)
|
||||
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
|
||||
addr1 = append(addr1, 33)
|
||||
require.ErrorContains(t, f(newCapsuleReader(t, typ, addr1)), "prefix length 33 exceeds IP address length (32)")
|
||||
})
|
||||
|
||||
t.Run("lower bits not covered by prefix length are not all zero", func(t *testing.T) {
|
||||
addr1 := quicvarint.Append(nil, 1337)
|
||||
addr1 = append(addr1, 4)
|
||||
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
|
||||
addr1 = append(addr1, 28)
|
||||
require.ErrorContains(t, f(newCapsuleReader(t, typ, addr1)), "lower bits not covered by prefix length are not all zero")
|
||||
})
|
||||
|
||||
t.Run("incomplete capsule", func(t *testing.T) {
|
||||
var data []byte
|
||||
switch typ {
|
||||
case capsuleTypeAddressAssign:
|
||||
data = (&addressAssignCapsule{
|
||||
AssignedAddresses: []AssignedAddress{
|
||||
{RequestID: 1337, IPPrefix: netip.MustParsePrefix("1.2.3.4/32")},
|
||||
{RequestID: 1338, IPPrefix: netip.MustParsePrefix("2001:db8::1/128")},
|
||||
},
|
||||
}).append(nil)
|
||||
case capsuleTypeAddressRequest:
|
||||
data = (&addressRequestCapsule{
|
||||
RequestIDs: []AddressRequestID{1337, 1338},
|
||||
Prefixes: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("2001:db8::1/128")},
|
||||
}).append(nil)
|
||||
default:
|
||||
t.Fatalf("unexpected capsule type: %d", typ)
|
||||
}
|
||||
|
||||
testIncompleteCapsule(t, data, f)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseAddressRequestCapsule(t *testing.T) {
|
||||
addr1 := quicvarint.Append(nil, 1337)
|
||||
addr1 = append(addr1, 4)
|
||||
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 0}).AsSlice()...)
|
||||
addr1 = append(addr1, 24)
|
||||
addr2 := quicvarint.Append(nil, 1338)
|
||||
addr2 = append(addr2, 6)
|
||||
addr2 = append(addr2, netip.MustParseAddr("2001:db8::1").AsSlice()...)
|
||||
addr2 = append(addr2, 128)
|
||||
data := quicvarint.Append(nil, uint64(capsuleTypeAddressRequest))
|
||||
data = quicvarint.Append(data, uint64(len(addr1)+len(addr2)))
|
||||
data = append(data, addr1...)
|
||||
data = append(data, addr2...)
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
typ, cr, err := http3.NewCapsuleParser(r).Next()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, capsuleTypeAddressRequest, typ)
|
||||
capsule, err := parseAddressRequestCapsule(cr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []AddressRequestID{1337, 1338}, capsule.RequestIDs)
|
||||
require.Equal(t, []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24"), netip.MustParsePrefix("2001:db8::1/128")}, capsule.Prefixes)
|
||||
require.Zero(t, r.Len())
|
||||
}
|
||||
|
||||
func TestParseAddressRequestCapsuleLimit(t *testing.T) {
|
||||
entry := []byte{1, 4, 192, 0, 2, 1, 32}
|
||||
testCapsuleEntryLimit(t, capsuleTypeAddressRequest, maxAddressesPerCapsule, func(int) []byte { return entry }, parseAddressRequestCapsule)
|
||||
}
|
||||
|
||||
func TestWriteAddressRequestCapsule(t *testing.T) {
|
||||
c := &addressRequestCapsule{
|
||||
RequestIDs: []AddressRequestID{1337, 1338},
|
||||
Prefixes: []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24"), netip.MustParsePrefix("2001:db8::1/128")},
|
||||
}
|
||||
data := c.append(nil)
|
||||
r := bytes.NewReader(data)
|
||||
typ, cr, err := http3.NewCapsuleParser(r).Next()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, capsuleTypeAddressRequest, typ)
|
||||
parsed, err := parseAddressRequestCapsule(cr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c, parsed)
|
||||
require.Zero(t, r.Len())
|
||||
}
|
||||
|
||||
func TestParseAddressRequestCapsuleInvalid(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
_, err := parseAddressRequestCapsule(newCapsuleReader(t, capsuleTypeAddressRequest, nil))
|
||||
require.ErrorContains(t, err, "contains no addresses")
|
||||
})
|
||||
t.Run("zero request ID", func(t *testing.T) {
|
||||
_, err := parseAddressRequestCapsule(newCapsuleReader(t, capsuleTypeAddressRequest, []byte{0, 4, 192, 0, 2, 1, 32}))
|
||||
require.ErrorContains(t, err, "zero request ID")
|
||||
})
|
||||
testParseAddressCapsuleInvalid(t, capsuleTypeAddressRequest, func(r http3.CapsuleReader) error {
|
||||
_, err := parseAddressRequestCapsule(r)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseRouteAdvertisementCapsule(t *testing.T) {
|
||||
iprange1 := []byte{4}
|
||||
iprange1 = append(iprange1, netip.AddrFrom4([4]byte{1, 1, 1, 1}).AsSlice()...)
|
||||
iprange1 = append(iprange1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
|
||||
iprange1 = append(iprange1, 13)
|
||||
iprange2 := []byte{6}
|
||||
iprange2 = append(iprange2, netip.MustParseAddr("2001:db8::1").AsSlice()...)
|
||||
iprange2 = append(iprange2, netip.MustParseAddr("2001:db8::100").AsSlice()...)
|
||||
iprange2 = append(iprange2, 37)
|
||||
|
||||
data := quicvarint.Append(nil, uint64(capsuleTypeRouteAdvertisement))
|
||||
data = quicvarint.Append(data, uint64(len(iprange1)+len(iprange2)))
|
||||
data = append(data, iprange1...)
|
||||
data = append(data, iprange2...)
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
typ, cr, err := http3.NewCapsuleParser(r).Next()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, capsuleTypeRouteAdvertisement, typ)
|
||||
capsule, err := parseRouteAdvertisementCapsule(cr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
[]IPRoute{
|
||||
{StartIP: netip.MustParseAddr("1.1.1.1"), EndIP: netip.MustParseAddr("1.2.3.4"), IPProtocol: 13},
|
||||
{StartIP: netip.MustParseAddr("2001:db8::1"), EndIP: netip.MustParseAddr("2001:db8::100"), IPProtocol: 37},
|
||||
},
|
||||
capsule.IPAddressRanges,
|
||||
)
|
||||
require.Equal(t,
|
||||
rangeToPrefixes(netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.2.3.4")),
|
||||
capsule.IPAddressRanges[0].Prefixes(),
|
||||
)
|
||||
require.Equal(t,
|
||||
rangeToPrefixes(netip.MustParseAddr("2001:db8::1"), netip.MustParseAddr("2001:db8::100")),
|
||||
capsule.IPAddressRanges[1].Prefixes(),
|
||||
)
|
||||
require.Zero(t, r.Len())
|
||||
}
|
||||
|
||||
func TestParseRouteAdvertisementCapsuleLimit(t *testing.T) {
|
||||
entry := func(i int) []byte { return []byte{4, 10, 0, byte(i >> 8), byte(i), 10, 0, byte(i >> 8), byte(i), 0} }
|
||||
testCapsuleEntryLimit(t, capsuleTypeRouteAdvertisement, maxRoutesPerCapsule, entry, parseRouteAdvertisementCapsule)
|
||||
}
|
||||
|
||||
func TestWriteRouteAdvertisementCapsule(t *testing.T) {
|
||||
c := &routeAdvertisementCapsule{
|
||||
IPAddressRanges: []IPRoute{
|
||||
{StartIP: netip.MustParseAddr("1.1.1.1"), EndIP: netip.MustParseAddr("1.2.3.4"), IPProtocol: 13},
|
||||
{StartIP: netip.MustParseAddr("2001:db8::1"), EndIP: netip.MustParseAddr("2001:db8::100"), IPProtocol: 37},
|
||||
},
|
||||
}
|
||||
data := c.append(nil)
|
||||
r := bytes.NewReader(data)
|
||||
typ, cr, err := http3.NewCapsuleParser(r).Next()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, capsuleTypeRouteAdvertisement, typ)
|
||||
parsed, err := parseRouteAdvertisementCapsule(cr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c, parsed)
|
||||
require.Zero(t, r.Len())
|
||||
}
|
||||
|
||||
func TestParseRouteAdvertisementCapsuleInvalid(t *testing.T) {
|
||||
t.Run("invalid IP version", func(t *testing.T) {
|
||||
iprange1 := []byte{5}
|
||||
iprange1 = append(iprange1, netip.AddrFrom4([4]byte{1, 1, 1, 1}).AsSlice()...)
|
||||
iprange1 = append(iprange1, netip.AddrFrom4([4]byte{1, 1, 1, 2}).AsSlice()...)
|
||||
iprange1 = append(iprange1, 13)
|
||||
_, err := parseRouteAdvertisementCapsule(newCapsuleReader(t, capsuleTypeRouteAdvertisement, iprange1))
|
||||
require.ErrorContains(t, err, "invalid IP version: 5")
|
||||
})
|
||||
|
||||
t.Run("start IP is greater than end IP", func(t *testing.T) {
|
||||
iprange1 := []byte{4}
|
||||
iprange1 = append(iprange1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
|
||||
iprange1 = append(iprange1, netip.AddrFrom4([4]byte{1, 1, 1, 1}).AsSlice()...)
|
||||
iprange1 = append(iprange1, 13)
|
||||
_, err := parseRouteAdvertisementCapsule(newCapsuleReader(t, capsuleTypeRouteAdvertisement, iprange1))
|
||||
require.ErrorContains(t, err, "start IP is greater than end IP")
|
||||
})
|
||||
|
||||
t.Run("incomplete capsule", func(t *testing.T) {
|
||||
data := (&routeAdvertisementCapsule{
|
||||
IPAddressRanges: []IPRoute{
|
||||
{StartIP: netip.MustParseAddr("1.1.1.1"), EndIP: netip.MustParseAddr("2.2.2.2"), IPProtocol: 13},
|
||||
{StartIP: netip.MustParseAddr("2001:db8::1"), EndIP: netip.MustParseAddr("2001:db8::100"), IPProtocol: 37},
|
||||
},
|
||||
}).append(nil)
|
||||
|
||||
testIncompleteCapsule(t, data, func(r http3.CapsuleReader) error {
|
||||
_, err := parseRouteAdvertisementCapsule(r)
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
route4a = IPRoute{StartIP: netip.MustParseAddr("10.0.0.0"), EndIP: netip.MustParseAddr("10.0.0.9")}
|
||||
route4b = IPRoute{StartIP: netip.MustParseAddr("10.0.0.10"), EndIP: netip.MustParseAddr("10.0.0.20")}
|
||||
route4ab = IPRoute{StartIP: netip.MustParseAddr("10.0.0.9"), EndIP: netip.MustParseAddr("10.0.0.20")}
|
||||
route6 = IPRoute{StartIP: netip.MustParseAddr("2001:db8::"), EndIP: netip.MustParseAddr("2001:db8::ffff")}
|
||||
)
|
||||
|
||||
func withProtocol(r IPRoute, proto uint8) IPRoute {
|
||||
r.IPProtocol = proto
|
||||
return r
|
||||
}
|
||||
|
||||
var routeOrderTests = []struct {
|
||||
name string
|
||||
routes []IPRoute
|
||||
err string
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "adjacent ranges", routes: []IPRoute{route4a, route4b}},
|
||||
{name: "IPv4 before IPv6 with a lower IP protocol", routes: []IPRoute{withProtocol(route4a, 17), route6}},
|
||||
{name: "same range for different IP protocols", routes: []IPRoute{withProtocol(route4a, 6), withProtocol(route4a, 17)}},
|
||||
{name: "IP protocol order before address order", routes: []IPRoute{withProtocol(route4b, 6), withProtocol(route4a, 17)}},
|
||||
{name: "IPv6 before IPv4", routes: []IPRoute{route6, route4a}, err: "not ordered by IP version and IP protocol"},
|
||||
{name: "descending IP protocols", routes: []IPRoute{withProtocol(route4a, 17), withProtocol(route4b, 6)}, err: "not ordered by IP version and IP protocol"},
|
||||
{name: "descending ranges", routes: []IPRoute{route4b, route4a}, err: "overlap or are not in ascending order"},
|
||||
{name: "overlapping ranges", routes: []IPRoute{route4a, route4ab}, err: "overlap or are not in ascending order"},
|
||||
{name: "duplicate range", routes: []IPRoute{route6, route6}, err: "overlap or are not in ascending order"},
|
||||
}
|
||||
|
||||
func TestParseRouteAdvertisementCapsuleOrder(t *testing.T) {
|
||||
for _, tc := range routeOrderTests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := (&routeAdvertisementCapsule{IPAddressRanges: tc.routes}).append(nil)
|
||||
_, cr, err := http3.NewCapsuleParser(bytes.NewReader(data)).Next()
|
||||
require.NoError(t, err)
|
||||
capsule, err := parseRouteAdvertisementCapsule(cr)
|
||||
if tc.err != "" {
|
||||
require.ErrorContains(t, err, tc.err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.routes, capsule.IPAddressRanges)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvertiseRouteValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
routes []IPRoute
|
||||
err string
|
||||
}{
|
||||
{name: "invalid start IP", routes: []IPRoute{{EndIP: route4a.EndIP}}, err: "invalid IP address range"},
|
||||
{name: "invalid end IP", routes: []IPRoute{{StartIP: route4a.StartIP}}, err: "invalid IP address range"},
|
||||
{
|
||||
name: "IPv6 zone",
|
||||
routes: []IPRoute{{StartIP: netip.MustParseAddr("fe80::1%eth0"), EndIP: netip.MustParseAddr("fe80::2%eth0")}},
|
||||
err: "invalid IP address range",
|
||||
},
|
||||
{name: "mixed IP versions", routes: []IPRoute{{StartIP: route4a.StartIP, EndIP: route6.EndIP}}, err: "mixes IP versions"},
|
||||
{
|
||||
name: "IPv4 and IPv4-mapped IPv6",
|
||||
routes: []IPRoute{{StartIP: netip.MustParseAddr("10.0.0.1"), EndIP: netip.MustParseAddr("::ffff:10.0.0.2")}},
|
||||
err: "mixes IP versions",
|
||||
},
|
||||
{name: "start after end", routes: []IPRoute{route4a, {StartIP: route4b.EndIP, EndIP: route4b.StartIP}}, err: "invalid route 1: start IP 10.0.0.20 is greater than end IP 10.0.0.10"},
|
||||
}
|
||||
tests = append(tests, routeOrderTests...)
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
err := conn.AdvertiseRoute(tc.routes)
|
||||
if tc.err != "" {
|
||||
require.ErrorContains(t, err, tc.err)
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
require.Empty(t, conn.queuedWrites)
|
||||
require.Nil(t, conn.localRoutes)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceiveMisorderedRouteAdvertisement(t *testing.T) {
|
||||
toRead := make(chan []byte, 1)
|
||||
conn := newProxiedConn(&mockStream{toRead: toRead})
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
|
||||
toRead <- (&routeAdvertisementCapsule{IPAddressRanges: []IPRoute{route6, route4a}}).append(nil)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
_, err := conn.Routes(ctx)
|
||||
require.ErrorIs(t, err, net.ErrClosed)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"log"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go/http3"
|
||||
)
|
||||
|
||||
var (
|
||||
tlsConf *tls.Config
|
||||
certPool *x509.CertPool
|
||||
)
|
||||
|
||||
func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
certTempl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2019),
|
||||
Subject: pkix.Name{},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
caPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &caPrivateKey.PublicKey, caPrivateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ca, err := x509.ParseCertificate(caBytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return ca, caPrivateKey, nil
|
||||
}
|
||||
|
||||
func generateLeafCert(ca *x509.Certificate, caPrivateKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
certTempl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
DNSNames: []string{"localhost", "127.0.0.1"},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, certTempl, ca, &privKey.PublicKey, caPrivateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return cert, privKey, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ca, caPrivateKey, err := generateCA()
|
||||
if err != nil {
|
||||
log.Fatal("failed to generate CA certificate:", err)
|
||||
}
|
||||
leafCert, leafPrivateKey, err := generateLeafCert(ca, caPrivateKey)
|
||||
if err != nil {
|
||||
log.Fatal("failed to generate leaf certificate:", err)
|
||||
}
|
||||
certPool = x509.NewCertPool()
|
||||
certPool.AddCert(ca)
|
||||
tlsConf = &tls.Config{
|
||||
Certificates: []tls.Certificate{{
|
||||
Certificate: [][]byte{leafCert.Raw},
|
||||
PrivateKey: leafPrivateKey,
|
||||
}},
|
||||
NextProtos: []string{http3.NextProtoH3},
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
func calculateIPv4Checksum(header []byte) uint16 {
|
||||
var sum uint32
|
||||
for i := 0; i < len(header); i += 2 {
|
||||
if i == 10 {
|
||||
continue
|
||||
}
|
||||
sum += uint32(binary.BigEndian.Uint16(header[i : i+2]))
|
||||
}
|
||||
for (sum >> 16) > 0 {
|
||||
sum = (sum & 0xffff) + (sum >> 16)
|
||||
}
|
||||
return ^uint16(sum)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIPv4ChecksumTestVector(t *testing.T) {
|
||||
data := []byte{0x45, 0x00, 0x00, 0x73, 0x00, 0x00, 0x40, 0x00, 0x40, 0x11, 0xb8, 0x61, 0xc0, 0xa8, 0x00, 0x01, 0xc0, 0xa8, 0x00, 0xc7}
|
||||
checksum := calculateIPv4Checksum(data)
|
||||
require.Equal(t, uint16(0xb861), checksum)
|
||||
}
|
||||
|
||||
func TestIPv4ChecksumWithOptions(t *testing.T) {
|
||||
data := []byte{0x46, 0x00, 0x00, 0x77, 0x00, 0x00, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x01, 0xc0, 0xa8, 0x00, 0xc7, 0x94, 0x04, 0x00, 0x00}
|
||||
checksum := calculateIPv4Checksum(data)
|
||||
data[10], data[11] = byte(checksum>>8), byte(checksum)
|
||||
require.True(t, ipv4ChecksumValid(data))
|
||||
require.NotEqual(t, checksum, calculateIPv4Checksum(data[:20]), "the options must be covered")
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
)
|
||||
|
||||
type ClientConn struct {
|
||||
clientConn *http3.ClientConn
|
||||
}
|
||||
|
||||
func NewClientConn(conn *http3.ClientConn) *ClientConn {
|
||||
return &ClientConn{clientConn: conn}
|
||||
}
|
||||
|
||||
func (c *ClientConn) Dial(req *Request) (*Conn, *http.Response, error) {
|
||||
httpReq := req.httpRequest()
|
||||
if httpReq.URL == nil {
|
||||
return nil, nil, errors.New("connect-ip: request URL is nil")
|
||||
}
|
||||
if httpReq.Host == "" && httpReq.URL.Host == "" {
|
||||
return nil, nil, errors.New("connect-ip: request needs a host")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-httpReq.Context().Done():
|
||||
return nil, nil, context.Cause(httpReq.Context())
|
||||
case <-c.clientConn.Context().Done():
|
||||
return nil, nil, context.Cause(c.clientConn.Context())
|
||||
case <-c.clientConn.ReceivedSettings():
|
||||
}
|
||||
|
||||
settings := c.clientConn.Settings()
|
||||
if !settings.EnableExtendedConnect {
|
||||
return nil, nil, errors.New("connect-ip: server didn't enable Extended CONNECT")
|
||||
}
|
||||
if !settings.EnableDatagrams {
|
||||
return nil, nil, errors.New("connect-ip: server didn't enable datagrams")
|
||||
}
|
||||
|
||||
rstr, err := c.clientConn.OpenRequestStream(httpReq.Context())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to open request stream: %w", err)
|
||||
}
|
||||
var keepStream bool
|
||||
defer func() {
|
||||
if !keepStream {
|
||||
rstr.CancelRead(quic.StreamErrorCode(http3.ErrCodeNoError))
|
||||
rstr.CancelWrite(quic.StreamErrorCode(http3.ErrCodeNoError))
|
||||
}
|
||||
}()
|
||||
if err := rstr.SendRequestHeader(httpReq); err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to send request: %w", err)
|
||||
}
|
||||
rsp, err := rstr.ReadResponse()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to read response: %w", err)
|
||||
}
|
||||
if rsp.StatusCode < 200 || rsp.StatusCode > 299 {
|
||||
return nil, rsp, fmt.Errorf("connect-ip: server responded with %d", rsp.StatusCode)
|
||||
}
|
||||
keepStream = true
|
||||
return newProxiedConn(rstr), rsp, nil
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClientWaitForSettings(t *testing.T) {
|
||||
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
ln, err := quic.Listen(conn, tlsConf, &quic.Config{EnableDatagrams: true})
|
||||
require.NoError(t, err)
|
||||
defer ln.Close()
|
||||
|
||||
h3conn := dialHTTP3(t, conn.LocalAddr().String())
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
req, err := NewRequest(ctx, "https://example.org/.well-known/masque/ip/")
|
||||
require.NoError(t, err)
|
||||
_, _, err = NewClientConn(h3conn).Dial(req)
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
func TestClientDatagramCheck(t *testing.T) {
|
||||
s := http3.Server{
|
||||
TLSConfig: tlsConf,
|
||||
QUICConfig: &quic.Config{EnableDatagrams: true},
|
||||
EnableDatagrams: false,
|
||||
}
|
||||
ln, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
go func() { s.Serve(ln) }()
|
||||
defer s.Close()
|
||||
|
||||
h3conn := dialHTTP3(t, ln.LocalAddr().String())
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
req, err := NewRequest(ctx, "https://example.org/.well-known/masque/ip/")
|
||||
require.NoError(t, err)
|
||||
_, _, err = NewClientConn(h3conn).Dial(req)
|
||||
require.ErrorContains(t, err, "connect-ip: server didn't enable datagrams")
|
||||
}
|
||||
|
||||
func TestNewClientConnSharesHTTP3Connection(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ln, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
require.NoError(t, err)
|
||||
defer ln.Close()
|
||||
url := "https://" + ln.LocalAddr().String()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/connect-ip", func(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := ParseProxyRequest(r)
|
||||
if !assert.NoError(t, err) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = (&Proxy{}).Proxy(w, req)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
mux.HandleFunc("GET /hello", func(http.ResponseWriter, *http.Request) {})
|
||||
s := http3.Server{Handler: mux, TLSConfig: tlsConf, EnableDatagrams: true}
|
||||
go func() { s.Serve(ln) }()
|
||||
defer s.Close()
|
||||
|
||||
h3conn := dialHTTP3(t, ln.LocalAddr().String())
|
||||
httpClient := &http.Client{Transport: h3conn, Timeout: time.Second}
|
||||
checkHTTP := func() {
|
||||
t.Helper()
|
||||
rsp, err := httpClient.Get(url + "/hello")
|
||||
require.NoError(t, err)
|
||||
rsp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, rsp.StatusCode)
|
||||
}
|
||||
|
||||
checkHTTP()
|
||||
req, err := NewRequest(ctx, url+"/connect-ip")
|
||||
require.NoError(t, err)
|
||||
tunnel, rsp, err := NewClientConn(h3conn).Dial(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, rsp.StatusCode)
|
||||
checkHTTP()
|
||||
require.NoError(t, tunnel.Close())
|
||||
checkHTTP()
|
||||
}
|
||||
@@ -1,620 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/apernet/quic-go/quicvarint"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
type CloseError struct {
|
||||
Remote bool
|
||||
}
|
||||
|
||||
func (e *CloseError) Error() string { return net.ErrClosed.Error() }
|
||||
func (e *CloseError) Is(target error) bool { return target == net.ErrClosed }
|
||||
|
||||
const (
|
||||
ipProtoICMP = 1
|
||||
ipProtoICMPv6 = 58
|
||||
)
|
||||
|
||||
type http3Stream interface {
|
||||
io.ReadWriteCloser
|
||||
StreamID() quic.StreamID
|
||||
ReceiveDatagram(context.Context) ([]byte, error)
|
||||
SendDatagram([]byte) error
|
||||
CancelRead(quic.StreamErrorCode)
|
||||
CancelWrite(quic.StreamErrorCode)
|
||||
SetWriteDeadline(time.Time) error
|
||||
}
|
||||
|
||||
var (
|
||||
_ http3Stream = &http3.Stream{}
|
||||
_ http3Stream = &http3.RequestStream{}
|
||||
)
|
||||
|
||||
const maxQueuedCapsules = 128
|
||||
|
||||
var errCapsuleLimit = goerrors.New("connect-ip: capsule limit exceeded")
|
||||
|
||||
type streamWrite struct {
|
||||
Data []byte
|
||||
Fin bool
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
str http3Stream
|
||||
writeNotify chan struct{}
|
||||
writeDone chan error
|
||||
|
||||
assignedAddressUpdates chan []AssignedAddress
|
||||
addressRequests chan *addressRequestCapsule
|
||||
availableRouteUpdates chan []IPRoute
|
||||
|
||||
mu sync.Mutex
|
||||
queuedWrites []streamWrite
|
||||
peerAddresses []netip.Prefix
|
||||
localRoutes []IPRoute
|
||||
assignedAddresses []netip.Prefix
|
||||
lastAddressRequestID AddressRequestID
|
||||
|
||||
closeChan chan struct{}
|
||||
closeErr error
|
||||
|
||||
closeOnce sync.Once
|
||||
closeResult error
|
||||
|
||||
datagramCapsuleOnce sync.Once
|
||||
}
|
||||
|
||||
func newProxiedConn(str http3Stream) *Conn {
|
||||
c := &Conn{
|
||||
str: str,
|
||||
writeNotify: make(chan struct{}, 1),
|
||||
writeDone: make(chan error, 1),
|
||||
assignedAddressUpdates: make(chan []AssignedAddress, maxQueuedCapsules),
|
||||
addressRequests: make(chan *addressRequestCapsule, maxQueuedCapsules),
|
||||
availableRouteUpdates: make(chan []IPRoute, 1),
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
go func() {
|
||||
err := c.readFromStream()
|
||||
c.mu.Lock()
|
||||
closing := c.closeErr != nil
|
||||
if !closing {
|
||||
c.closeErr = &CloseError{Remote: true}
|
||||
close(c.closeChan)
|
||||
if err != nil {
|
||||
code := http3.ErrCodeMessageError
|
||||
var streamErr *quic.StreamError
|
||||
var h3Err *http3.Error
|
||||
switch {
|
||||
case goerrors.Is(err, errCapsuleLimit):
|
||||
code = http3.ErrCodeExcessiveLoad
|
||||
case goerrors.As(err, &streamErr) && streamErr.Remote, goerrors.As(err, &h3Err) && h3Err.Remote:
|
||||
code = http3.ErrCodeRequestCanceled
|
||||
}
|
||||
c.str.CancelRead(quic.StreamErrorCode(code))
|
||||
c.str.CancelWrite(quic.StreamErrorCode(code))
|
||||
close(c.writeNotify)
|
||||
} else {
|
||||
c.queueFin()
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if err != nil && !closing {
|
||||
errors.LogInfoInner(context.Background(), err, "reading capsules failed")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := c.writeToStream()
|
||||
if err != nil {
|
||||
c.mu.Lock()
|
||||
closing := c.closeErr != nil
|
||||
if !closing {
|
||||
c.closeErr = &CloseError{Remote: true}
|
||||
close(c.closeChan)
|
||||
c.str.CancelRead(quic.StreamErrorCode(http3.ErrCodeExcessiveLoad))
|
||||
c.str.CancelWrite(quic.StreamErrorCode(http3.ErrCodeExcessiveLoad))
|
||||
} else {
|
||||
c.str.CancelWrite(quic.StreamErrorCode(http3.ErrCodeNoError))
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if !closing {
|
||||
errors.LogInfoInner(context.Background(), err, "writing capsules failed")
|
||||
}
|
||||
}
|
||||
c.writeDone <- err
|
||||
close(c.writeDone)
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Conn) AdvertiseRoute(routes []IPRoute) error {
|
||||
for i, route := range routes {
|
||||
err := route.validate()
|
||||
if err == nil && i > 0 {
|
||||
err = checkRouteOrder(routes[i-1], route)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect-ip: invalid route %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.closeErr != nil {
|
||||
err := c.closeErr
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
routes = slices.Clone(routes)
|
||||
err := c.queueWrite(streamWrite{Data: (&routeAdvertisementCapsule{IPAddressRanges: routes}).append(nil)})
|
||||
if err == nil {
|
||||
c.localRoutes = routes
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) RequestAddresses(prefixes []netip.Prefix) ([]AddressRequestID, error) {
|
||||
if len(prefixes) == 0 {
|
||||
return nil, goerrors.New("connect-ip: address request must contain at least one prefix")
|
||||
}
|
||||
for i, p := range prefixes {
|
||||
if !p.IsValid() || p != p.Masked() {
|
||||
return nil, fmt.Errorf("connect-ip: invalid requested prefix %d: %s", i, p)
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.closeErr != nil {
|
||||
err := c.closeErr
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]AddressRequestID, len(prefixes))
|
||||
for i := range ids {
|
||||
ids[i] = c.lastAddressRequestID + AddressRequestID(i) + 1
|
||||
}
|
||||
capsule := &addressRequestCapsule{RequestIDs: ids, Prefixes: prefixes}
|
||||
err := c.queueWrite(streamWrite{Data: capsule.append(nil)})
|
||||
if err == nil {
|
||||
c.lastAddressRequestID = ids[len(ids)-1]
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (c *Conn) ReceiveAddressAssignment(ctx context.Context) ([]AssignedAddress, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case assignment := <-c.assignedAddressUpdates:
|
||||
return assignment, nil
|
||||
case <-c.closeChan:
|
||||
select {
|
||||
case assignment := <-c.assignedAddressUpdates:
|
||||
return assignment, nil
|
||||
default:
|
||||
return nil, c.closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) ReceiveAddressRequest(ctx context.Context) (*AddressRequest, error) {
|
||||
var requested *addressRequestCapsule
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case requested = <-c.addressRequests:
|
||||
case <-c.closeChan:
|
||||
select {
|
||||
case requested = <-c.addressRequests:
|
||||
default:
|
||||
return nil, c.closeErr
|
||||
}
|
||||
}
|
||||
return newAddressRequest(c, requested), nil
|
||||
}
|
||||
|
||||
func (c *Conn) AssignAddresses(prefixes []netip.Prefix) error {
|
||||
capsule := &addressAssignCapsule{}
|
||||
if prefixes != nil {
|
||||
capsule.AssignedAddresses = make([]AssignedAddress, len(prefixes))
|
||||
for i, p := range prefixes {
|
||||
capsule.AssignedAddresses[i] = AssignedAddress{IPPrefix: p}
|
||||
}
|
||||
}
|
||||
return c.sendAddressAssignment(capsule, true)
|
||||
}
|
||||
|
||||
func (c *Conn) sendAddressAssignment(capsule *addressAssignCapsule, restrictPeer bool) error {
|
||||
c.mu.Lock()
|
||||
if c.closeErr != nil {
|
||||
err := c.closeErr
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
if err := c.queueWrite(streamWrite{Data: capsule.append(nil)}); err != nil {
|
||||
c.mu.Unlock()
|
||||
c.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if !restrictPeer && c.peerAddresses == nil {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
var prefixes []netip.Prefix
|
||||
if capsule.AssignedAddresses != nil {
|
||||
prefixes = make([]netip.Prefix, 0, len(capsule.AssignedAddresses))
|
||||
}
|
||||
for _, assigned := range capsule.AssignedAddresses {
|
||||
if !assigned.Rejected() {
|
||||
prefixes = append(prefixes, assigned.IPPrefix)
|
||||
}
|
||||
}
|
||||
c.peerAddresses = prefixes
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) queueWrite(w streamWrite) error {
|
||||
if len(c.queuedWrites) >= maxQueuedCapsules {
|
||||
c.closeErr = &CloseError{Remote: false}
|
||||
close(c.closeChan)
|
||||
c.str.CancelRead(quic.StreamErrorCode(http3.ErrCodeExcessiveLoad))
|
||||
c.str.CancelWrite(quic.StreamErrorCode(http3.ErrCodeExcessiveLoad))
|
||||
close(c.writeNotify)
|
||||
return goerrors.New("connect-ip: capsule queue full")
|
||||
}
|
||||
c.queuedWrites = append(c.queuedWrites, w)
|
||||
c.notifyWriter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) queueFin() {
|
||||
c.str.SetWriteDeadline(time.Now())
|
||||
c.queuedWrites = append(c.queuedWrites, streamWrite{Fin: true})
|
||||
c.notifyWriter()
|
||||
}
|
||||
|
||||
func (c *Conn) notifyWriter() {
|
||||
select {
|
||||
case c.writeNotify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func queueLatest[T any](ch chan T, value T) {
|
||||
for {
|
||||
select {
|
||||
case ch <- value:
|
||||
return
|
||||
case <-ch:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Routes(ctx context.Context) ([]IPRoute, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-c.closeChan:
|
||||
return nil, c.closeErr
|
||||
case routes := <-c.availableRouteUpdates:
|
||||
return routes, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) readFromStream() error {
|
||||
p := http3.NewCapsuleParser(c.str)
|
||||
for {
|
||||
t, cr, err := p.Next()
|
||||
if goerrors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t {
|
||||
case capsuleTypeAddressAssign:
|
||||
capsule, err := parseAddressAssignCapsule(cr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prefixes := make([]netip.Prefix, 0, len(capsule.AssignedAddresses))
|
||||
for _, assigned := range capsule.AssignedAddresses {
|
||||
if !assigned.Rejected() {
|
||||
prefixes = append(prefixes, assigned.IPPrefix)
|
||||
}
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.assignedAddresses = prefixes
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case c.assignedAddressUpdates <- capsule.AssignedAddresses:
|
||||
default:
|
||||
return fmt.Errorf("%w: address assignment queue full", errCapsuleLimit)
|
||||
}
|
||||
case capsuleTypeAddressRequest:
|
||||
capsule, err := parseAddressRequestCapsule(cr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case c.addressRequests <- capsule:
|
||||
default:
|
||||
return fmt.Errorf("%w: address request queue full", errCapsuleLimit)
|
||||
}
|
||||
case capsuleTypeRouteAdvertisement:
|
||||
capsule, err := parseRouteAdvertisementCapsule(cr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queueLatest(c.availableRouteUpdates, capsule.IPAddressRanges)
|
||||
case capsuleTypeDatagram:
|
||||
c.datagramCapsuleOnce.Do(func() {
|
||||
errors.LogWarning(context.Background(), "connect-ip: dropping IP packets sent in DATAGRAM capsules, only QUIC DATAGRAM frames are supported")
|
||||
})
|
||||
if err := cr.Discard(); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := cr.Discard(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) writeToStream() error {
|
||||
for range c.writeNotify {
|
||||
for {
|
||||
c.mu.Lock()
|
||||
if len(c.queuedWrites) == 0 {
|
||||
c.mu.Unlock()
|
||||
break
|
||||
}
|
||||
w := c.queuedWrites[0]
|
||||
c.queuedWrites[0] = streamWrite{}
|
||||
c.queuedWrites = c.queuedWrites[1:]
|
||||
c.mu.Unlock()
|
||||
|
||||
if w.Fin {
|
||||
return c.str.Close()
|
||||
}
|
||||
if _, err := c.str.Write(w.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.closeErr
|
||||
}
|
||||
|
||||
func (c *Conn) ReadPacket(b []byte) (int, error) {
|
||||
for {
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return 0, c.closeErr
|
||||
default:
|
||||
}
|
||||
data, err := c.str.ReceiveDatagram(context.Background())
|
||||
if err != nil {
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return 0, c.closeErr
|
||||
default:
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
contextID, n, err := quicvarint.Parse(data)
|
||||
if err != nil {
|
||||
errors.LogDebugInner(context.Background(), err, "dropping malformed datagram")
|
||||
continue
|
||||
}
|
||||
if contextID != 0 {
|
||||
continue
|
||||
}
|
||||
packet := data[n:]
|
||||
if err := c.handleIncomingProxiedPacket(packet); err != nil {
|
||||
errors.LogDebugInner(context.Background(), err, "dropping proxied packet")
|
||||
continue
|
||||
}
|
||||
if len(packet) > len(b) {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
return copy(b, packet), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) handleIncomingProxiedPacket(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return goerrors.New("connect-ip: empty packet")
|
||||
}
|
||||
var src, dst netip.Addr
|
||||
var ipProto uint8
|
||||
switch v := ipVersion(data); v {
|
||||
default:
|
||||
return fmt.Errorf("connect-ip: unknown IP versions: %d", v)
|
||||
case 4:
|
||||
if len(data) < ipv4.HeaderLen {
|
||||
return fmt.Errorf("connect-ip: malformed datagram: too short")
|
||||
}
|
||||
src = netip.AddrFrom4([4]byte(data[12:16]))
|
||||
dst = netip.AddrFrom4([4]byte(data[16:20]))
|
||||
ipProto = data[9]
|
||||
case 6:
|
||||
if len(data) < ipv6.HeaderLen {
|
||||
return fmt.Errorf("connect-ip: malformed datagram: too short")
|
||||
}
|
||||
src = netip.AddrFrom16([16]byte(data[8:24]))
|
||||
dst = netip.AddrFrom16([16]byte(data[24:40]))
|
||||
ipProto = data[6]
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
assignedAddresses := c.assignedAddresses
|
||||
localRoutes := c.localRoutes
|
||||
peerAddresses := c.peerAddresses
|
||||
c.mu.Unlock()
|
||||
|
||||
if peerAddresses != nil {
|
||||
if !slices.ContainsFunc(peerAddresses, func(p netip.Prefix) bool { return p.Contains(src) }) {
|
||||
return fmt.Errorf("connect-ip: datagram source address not allowed: %s", src)
|
||||
}
|
||||
}
|
||||
|
||||
var isAllowedDst bool
|
||||
if len(assignedAddresses) > 0 {
|
||||
isAllowedDst = slices.ContainsFunc(assignedAddresses, func(p netip.Prefix) bool { return p.Contains(dst) })
|
||||
}
|
||||
if !isAllowedDst {
|
||||
isAllowedDst = slices.ContainsFunc(localRoutes, func(r IPRoute) bool {
|
||||
if r.StartIP.Compare(dst) > 0 || dst.Compare(r.EndIP) > 0 {
|
||||
return false
|
||||
}
|
||||
if (ipVersion(data) == 4 && ipProto == ipProtoICMP) || (ipVersion(data) == 6 && ipProto == ipProtoICMPv6) {
|
||||
return true
|
||||
}
|
||||
return r.IPProtocol == 0 || r.IPProtocol == ipProto
|
||||
})
|
||||
}
|
||||
if !isAllowedDst {
|
||||
return fmt.Errorf("connect-ip: datagram destination address / protocol not allowed: %s (protocol: %d)", dst, ipProto)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) WritePacket(b []byte) (icmp []byte, err error) {
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return nil, c.closeErr
|
||||
default:
|
||||
}
|
||||
data, err := c.composeDatagram(b)
|
||||
if err != nil {
|
||||
errors.LogDebugInner(context.Background(), err, "dropping proxied packet (", len(b), " bytes) that can't be proxied")
|
||||
return nil, nil
|
||||
}
|
||||
if err := c.str.SendDatagram(data); err != nil {
|
||||
if tooLarge, ok := goerrors.AsType[*quic.DatagramTooLargeError](err); ok {
|
||||
icmpPacket, err := composeICMPTooLargePacket(b, int(tooLarge.MaxDatagramPayloadSize)-c.datagramOverhead())
|
||||
if err != nil {
|
||||
if goerrors.Is(err, ErrMTUTooSmall) {
|
||||
return nil, err
|
||||
}
|
||||
errors.LogDebugInner(context.Background(), err, "failed to compose ICMP Packet Too Big")
|
||||
}
|
||||
return icmpPacket, nil
|
||||
}
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return nil, c.closeErr
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Conn) composeDatagram(b []byte) ([]byte, error) {
|
||||
if len(b) == 0 {
|
||||
return nil, goerrors.New("connect-ip: empty packet")
|
||||
}
|
||||
switch v := ipVersion(b); v {
|
||||
default:
|
||||
return nil, fmt.Errorf("connect-ip: unknown IP versions: %d", v)
|
||||
case 4:
|
||||
if len(b) < ipv4.HeaderLen {
|
||||
return nil, fmt.Errorf("connect-ip: IPv4 packet too short")
|
||||
}
|
||||
hdrLen := int(b[0]&0x0f) << 2
|
||||
totalLen := int(binary.BigEndian.Uint16(b[2:4]))
|
||||
if hdrLen < ipv4.HeaderLen || hdrLen > totalLen || totalLen > len(b) {
|
||||
return nil, fmt.Errorf("connect-ip: malformed IPv4 header: header length %d, total length %d, packet length %d", hdrLen, totalLen, len(b))
|
||||
}
|
||||
ttl := b[8]
|
||||
if ttl <= 1 {
|
||||
return nil, fmt.Errorf("connect-ip: datagram TTL too small: %d", ttl)
|
||||
}
|
||||
b[8]--
|
||||
binary.BigEndian.PutUint16(b[10:12], calculateIPv4Checksum(b[:hdrLen]))
|
||||
case 6:
|
||||
if len(b) < ipv6.HeaderLen {
|
||||
return nil, fmt.Errorf("connect-ip: IPv6 packet too short")
|
||||
}
|
||||
hopLimit := b[7]
|
||||
if hopLimit <= 1 {
|
||||
return nil, fmt.Errorf("connect-ip: datagram Hop Limit too small: %d", hopLimit)
|
||||
}
|
||||
b[7]--
|
||||
}
|
||||
data := make([]byte, 0, len(contextIDZero)+len(b))
|
||||
data = append(data, contextIDZero...)
|
||||
data = append(data, b...)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Conn) datagramOverhead() int {
|
||||
return quicvarint.Len(uint64(c.str.StreamID()/4)) + len(contextIDZero)
|
||||
}
|
||||
|
||||
func (c *Conn) MaxPacketSize() int {
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return 0
|
||||
default:
|
||||
}
|
||||
err := c.str.SendDatagram(make([]byte, 1<<16))
|
||||
tooLarge, ok := goerrors.AsType[*quic.DatagramTooLargeError](err)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return max(0, int(tooLarge.MaxDatagramPayloadSize)-c.datagramOverhead())
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
c.mu.Lock()
|
||||
if c.closeErr == nil {
|
||||
c.closeErr = &CloseError{Remote: false}
|
||||
close(c.closeChan)
|
||||
c.queueFin()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.closeResult = <-c.writeDone
|
||||
c.str.CancelRead(quic.StreamErrorCode(http3.ErrCodeNoError))
|
||||
})
|
||||
return c.closeResult
|
||||
}
|
||||
|
||||
func ipVersion(b []byte) uint8 { return b[0] >> 4 }
|
||||
@@ -1,706 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/apernet/quic-go/quicvarint"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
var ipv6Header = []byte{
|
||||
0x60, 0x00, 0x00, 0x00,
|
||||
0x00, 0x20, 59, 64,
|
||||
0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x08, 0xd3, 0x13, 0x19, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x48,
|
||||
}
|
||||
|
||||
var (
|
||||
testSrc4 = netip.MustParseAddr("192.0.2.1")
|
||||
testDst4 = netip.MustParseAddr("198.51.100.1")
|
||||
testSrc6 = netip.MustParseAddr("2001:db8::1")
|
||||
testDst6 = netip.MustParseAddr("2001:db8:1::1")
|
||||
)
|
||||
|
||||
func ipv4Packet(ttl, proto uint8, src, dst netip.Addr, options, payload []byte) []byte {
|
||||
hdrLen := ipv4.HeaderLen + len(options)
|
||||
b := make([]byte, hdrLen, hdrLen+len(payload))
|
||||
b[0] = 4<<4 | byte(hdrLen>>2)
|
||||
binary.BigEndian.PutUint16(b[2:4], uint16(hdrLen+len(payload)))
|
||||
b[8] = ttl
|
||||
b[9] = proto
|
||||
copy(b[12:16], src.AsSlice())
|
||||
copy(b[16:20], dst.AsSlice())
|
||||
copy(b[ipv4.HeaderLen:], options)
|
||||
return append(b, payload...)
|
||||
}
|
||||
|
||||
func ipv6Packet(hopLimit, nextHeader uint8, src, dst netip.Addr, payload []byte) []byte {
|
||||
b := make([]byte, ipv6.HeaderLen, ipv6.HeaderLen+len(payload))
|
||||
b[0] = 6 << 4
|
||||
binary.BigEndian.PutUint16(b[4:6], uint16(len(payload)))
|
||||
b[6] = nextHeader
|
||||
b[7] = hopLimit
|
||||
copy(b[8:24], src.AsSlice())
|
||||
copy(b[24:40], dst.AsSlice())
|
||||
return append(b, payload...)
|
||||
}
|
||||
|
||||
func ipv4ChecksumValid(header []byte) bool {
|
||||
var sum uint32
|
||||
for i := 0; i+1 < len(header); i += 2 {
|
||||
sum += uint32(binary.BigEndian.Uint16(header[i:]))
|
||||
}
|
||||
for sum > 0xffff {
|
||||
sum = sum&0xffff + sum>>16
|
||||
}
|
||||
return sum == 0xffff
|
||||
}
|
||||
|
||||
type mockStream struct {
|
||||
streamID quic.StreamID
|
||||
reading []byte
|
||||
toRead <-chan []byte
|
||||
datagrams <-chan []byte
|
||||
maxDatagramPayloadSize int
|
||||
sendDatagramErr error
|
||||
sent [][]byte
|
||||
writeStarted chan struct{}
|
||||
written chan<- []byte
|
||||
readErr error
|
||||
|
||||
mu sync.Mutex
|
||||
cancelWriteCodes []quic.StreamErrorCode
|
||||
}
|
||||
|
||||
func (m *mockStream) cancelWriteCode() (quic.StreamErrorCode, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if len(m.cancelWriteCodes) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return m.cancelWriteCodes[0], true
|
||||
}
|
||||
|
||||
var _ http3Stream = &mockStream{}
|
||||
|
||||
func (m *mockStream) StreamID() quic.StreamID { return m.streamID }
|
||||
func (m *mockStream) Read(p []byte) (int, error) {
|
||||
if len(m.reading) == 0 && m.readErr != nil {
|
||||
return 0, m.readErr
|
||||
}
|
||||
if len(m.reading) == 0 {
|
||||
m.reading = <-m.toRead
|
||||
}
|
||||
n := copy(p, m.reading)
|
||||
m.reading = m.reading[n:]
|
||||
return n, nil
|
||||
}
|
||||
func (m *mockStream) CancelRead(quic.StreamErrorCode) {}
|
||||
func (m *mockStream) Write(p []byte) (int, error) {
|
||||
if m.writeStarted != nil {
|
||||
close(m.writeStarted)
|
||||
m.writeStarted = nil
|
||||
}
|
||||
if m.written != nil {
|
||||
m.written <- bytes.Clone(p)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
func (m *mockStream) Close() error { return nil }
|
||||
func (m *mockStream) CancelWrite(code quic.StreamErrorCode) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.cancelWriteCodes = append(m.cancelWriteCodes, code)
|
||||
}
|
||||
func (m *mockStream) SetWriteDeadline(time.Time) error { return nil }
|
||||
func (m *mockStream) SendDatagram(data []byte) error {
|
||||
if m.sendDatagramErr != nil {
|
||||
return m.sendDatagramErr
|
||||
}
|
||||
if size := quicvarint.Len(uint64(m.streamID/4)) + len(data); m.maxDatagramPayloadSize > 0 && size > m.maxDatagramPayloadSize {
|
||||
return &quic.DatagramTooLargeError{MaxDatagramPayloadSize: int64(m.maxDatagramPayloadSize)}
|
||||
}
|
||||
m.sent = append(m.sent, bytes.Clone(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockStream) ReceiveDatagram(ctx context.Context) ([]byte, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case data, ok := <-m.datagrams:
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapsuleWriteQueueLimit(t *testing.T) {
|
||||
writes := make(chan []byte)
|
||||
writeStarted := make(chan struct{})
|
||||
conn := newProxiedConn(&mockStream{
|
||||
writeStarted: writeStarted,
|
||||
written: writes,
|
||||
})
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
|
||||
require.NoError(t, conn.AssignAddresses(nil))
|
||||
select {
|
||||
case <-writeStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("capsule write did not start")
|
||||
}
|
||||
|
||||
for range maxQueuedCapsules {
|
||||
require.NoError(t, conn.AssignAddresses(nil))
|
||||
}
|
||||
go func() {
|
||||
conn.Routes(context.Background())
|
||||
for range maxQueuedCapsules + 1 {
|
||||
<-writes
|
||||
}
|
||||
}()
|
||||
require.ErrorContains(t, conn.AssignAddresses(nil), "capsule queue full")
|
||||
require.ErrorIs(t, conn.AssignAddresses(nil), net.ErrClosed)
|
||||
}
|
||||
|
||||
func TestCapsuleReceiveQueueLimit(t *testing.T) {
|
||||
for _, name := range []string{"assignments", "requests"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var data []byte
|
||||
for i := range maxQueuedCapsules + 1 {
|
||||
if name == "assignments" {
|
||||
data = (&addressAssignCapsule{}).append(data)
|
||||
} else {
|
||||
data = (&addressRequestCapsule{
|
||||
RequestIDs: []AddressRequestID{AddressRequestID(i + 1)},
|
||||
Prefixes: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")},
|
||||
}).append(data)
|
||||
}
|
||||
}
|
||||
conn := newProxiedConn(&mockStream{reading: data})
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
_, err := conn.Routes(ctx)
|
||||
require.ErrorIs(t, err, net.ErrClosed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbortErrorCode(t *testing.T) {
|
||||
var overflow []byte
|
||||
for range maxQueuedCapsules + 1 {
|
||||
overflow = (&addressAssignCapsule{}).append(overflow)
|
||||
}
|
||||
misordered := (&routeAdvertisementCapsule{IPAddressRanges: []IPRoute{
|
||||
{StartIP: netip.MustParseAddr("192.0.2.2"), EndIP: netip.MustParseAddr("192.0.2.1")},
|
||||
}}).append(nil)
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
str *mockStream
|
||||
code http3.ErrCode
|
||||
}{
|
||||
{"malformed capsule", &mockStream{reading: misordered}, http3.ErrCodeMessageError},
|
||||
{"queue limit", &mockStream{reading: overflow}, http3.ErrCodeExcessiveLoad},
|
||||
{"reset by peer", &mockStream{readErr: &quic.StreamError{ErrorCode: quic.StreamErrorCode(http3.ErrCodeNoError), Remote: true}}, http3.ErrCodeRequestCanceled},
|
||||
{"reset by peer on a request stream", &mockStream{readErr: &http3.Error{ErrorCode: http3.ErrCodeNoError, Remote: true}}, http3.ErrCodeRequestCanceled},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
conn := newProxiedConn(c.str)
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
require.Eventually(t, func() bool {
|
||||
_, ok := c.str.cancelWriteCode()
|
||||
return ok
|
||||
}, time.Second, time.Millisecond)
|
||||
code, _ := c.str.cancelWriteCode()
|
||||
require.Equal(t, quic.StreamErrorCode(c.code), code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncomingDatagrams(t *testing.T) {
|
||||
t.Run("empty packets", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
require.ErrorContains(t,
|
||||
conn.handleIncomingProxiedPacket([]byte{}),
|
||||
"connect-ip: empty packet",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("invalid IP version", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
data := make([]byte, 20)
|
||||
data[0] = 5 << 4
|
||||
require.ErrorContains(t,
|
||||
conn.handleIncomingProxiedPacket(data),
|
||||
"connect-ip: unknown IP versions: 5",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("IPv4 packet too short", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
data, err := (&ipv4.Header{
|
||||
Src: net.IPv4(1, 2, 3, 4),
|
||||
Dst: net.IPv4(159, 70, 42, 98),
|
||||
Len: 20,
|
||||
Checksum: 89,
|
||||
}).Marshal()
|
||||
require.NoError(t, err)
|
||||
require.ErrorContains(t,
|
||||
conn.handleIncomingProxiedPacket(data[:ipv4.HeaderLen-1]),
|
||||
"connect-ip: malformed datagram: too short",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("IPv6 packet too short", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
require.ErrorContains(t,
|
||||
conn.handleIncomingProxiedPacket(ipv6Header[:ipv6.HeaderLen-1]),
|
||||
"connect-ip: malformed datagram: too short",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("invalid source address", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
require.NoError(t, conn.AssignAddresses([]netip.Prefix{netip.MustParsePrefix("192.168.0.10/32")}))
|
||||
hdr := &ipv4.Header{
|
||||
Src: net.IPv4(192, 168, 0, 11),
|
||||
Dst: net.IPv4(159, 70, 42, 98),
|
||||
Len: 20,
|
||||
Checksum: 89,
|
||||
}
|
||||
data, err := hdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.ErrorContains(t,
|
||||
conn.handleIncomingProxiedPacket(data),
|
||||
"connect-ip: datagram source address not allowed: 192.168.0.11",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("invalid destination address", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
require.NoError(t, conn.AssignAddresses([]netip.Prefix{netip.MustParsePrefix("192.168.0.10/32")}))
|
||||
require.NoError(t, conn.AdvertiseRoute([]IPRoute{
|
||||
{StartIP: netip.MustParseAddr("10.0.0.0"), EndIP: netip.MustParseAddr("10.1.2.3")},
|
||||
}))
|
||||
hdr := &ipv4.Header{
|
||||
Src: net.IPv4(192, 168, 0, 10),
|
||||
Dst: net.IPv4(10, 1, 2, 3),
|
||||
Len: 20,
|
||||
Checksum: 89,
|
||||
}
|
||||
data, err := hdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, conn.handleIncomingProxiedPacket(data))
|
||||
|
||||
hdr.Dst = net.IPv4(10, 1, 2, 4)
|
||||
data, err = hdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.ErrorContains(t,
|
||||
conn.handleIncomingProxiedPacket(data),
|
||||
"connect-ip: datagram destination address / protocol not allowed: 10.1.2.4 (protocol: 0)",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("invalid IP protocol", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
require.NoError(t, conn.AssignAddresses([]netip.Prefix{netip.MustParsePrefix("192.168.0.10/32")}))
|
||||
require.NoError(t, conn.AdvertiseRoute([]IPRoute{
|
||||
{StartIP: netip.MustParseAddr("10.0.0.0"), EndIP: netip.MustParseAddr("10.1.2.3"), IPProtocol: 42},
|
||||
}))
|
||||
hdr := &ipv4.Header{
|
||||
Src: net.IPv4(192, 168, 0, 10),
|
||||
Dst: net.IPv4(10, 1, 2, 3),
|
||||
Len: 20,
|
||||
Checksum: 89,
|
||||
Protocol: 42,
|
||||
}
|
||||
data, err := hdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, conn.handleIncomingProxiedPacket(data))
|
||||
|
||||
hdr.Protocol = 41
|
||||
data, err = hdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.ErrorContains(t,
|
||||
conn.handleIncomingProxiedPacket(data),
|
||||
"connect-ip: datagram destination address / protocol not allowed: 10.1.2.3 (protocol: 41)",
|
||||
)
|
||||
|
||||
hdr.Protocol = ipProtoICMP
|
||||
data, err = hdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, conn.handleIncomingProxiedPacket(data))
|
||||
})
|
||||
|
||||
t.Run("packet from assigned address", func(t *testing.T) {
|
||||
readChan := make(chan []byte, 1)
|
||||
conn := newProxiedConn(&mockStream{toRead: readChan})
|
||||
|
||||
hdr := &ipv4.Header{
|
||||
Src: net.IPv4(159, 70, 42, 98),
|
||||
Dst: net.IPv4(192, 168, 0, 10),
|
||||
Len: 20,
|
||||
Checksum: 89,
|
||||
}
|
||||
data, err := hdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.Error(t, conn.handleIncomingProxiedPacket(data), "connect-ip: datagram destination address")
|
||||
|
||||
readChan <- (&addressAssignCapsule{
|
||||
AssignedAddresses: []AssignedAddress{{IPPrefix: netip.MustParsePrefix("192.168.0.10/32")}},
|
||||
}).append(nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
_, err = conn.ReceiveAddressAssignment(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, conn.handleIncomingProxiedPacket(data))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSkipUnknownCapsule(t *testing.T) {
|
||||
for _, typ := range []http3.CapsuleType{42, capsuleTypeDatagram} {
|
||||
readChan := make(chan []byte, 1)
|
||||
conn := newProxiedConn(&mockStream{toRead: readChan})
|
||||
|
||||
data := quicvarint.Append(nil, uint64(typ))
|
||||
data = quicvarint.Append(data, 3)
|
||||
data = append(data, "foo"...)
|
||||
data = (&addressAssignCapsule{
|
||||
AssignedAddresses: []AssignedAddress{{IPPrefix: netip.MustParsePrefix("192.168.0.10/32")}},
|
||||
}).append(data)
|
||||
readChan <- data
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
assigned, err := conn.ReceiveAddressAssignment(ctx)
|
||||
cancel()
|
||||
require.NoError(t, err, "capsule type %d", typ)
|
||||
require.Equal(t, []AssignedAddress{{IPPrefix: netip.MustParsePrefix("192.168.0.10/32")}}, assigned)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzIncomingDatagram(f *testing.F) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
require.NoError(f, conn.AssignAddresses([]netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/16"),
|
||||
netip.MustParsePrefix("2001:db8::0/64"),
|
||||
}))
|
||||
require.NoError(f, conn.AdvertiseRoute([]IPRoute{
|
||||
{StartIP: netip.MustParseAddr("10.0.0.0"), EndIP: netip.MustParseAddr("10.1.2.3"), IPProtocol: 42},
|
||||
{StartIP: netip.MustParseAddr("2001:db8:1::"), EndIP: netip.MustParseAddr("2001:db8:1::ffff"), IPProtocol: 42},
|
||||
}))
|
||||
|
||||
ipv4Header, err := (&ipv4.Header{
|
||||
Src: net.IPv4(1, 2, 3, 4),
|
||||
Dst: net.IPv4(159, 70, 42, 98),
|
||||
Len: 20,
|
||||
Checksum: 89,
|
||||
}).Marshal()
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(ipv4Header)
|
||||
f.Add(ipv6Header)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
conn.handleIncomingProxiedPacket(data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendingDatagrams(t *testing.T) {
|
||||
t.Run("invalid IP version", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
data := make([]byte, 20)
|
||||
data[0] = 5 << 4
|
||||
_, err := conn.composeDatagram(data)
|
||||
require.ErrorContains(t, err, "connect-ip: unknown IP versions: 5")
|
||||
})
|
||||
|
||||
t.Run("IPv4 packet too short", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
data, err := (&ipv4.Header{
|
||||
Src: net.IPv4(1, 2, 3, 4),
|
||||
Dst: net.IPv4(159, 70, 42, 98),
|
||||
Len: 20,
|
||||
Checksum: 89,
|
||||
}).Marshal()
|
||||
require.NoError(t, err)
|
||||
_, err = conn.composeDatagram(data[:ipv4.HeaderLen-1])
|
||||
require.ErrorContains(t, err, "connect-ip: IPv4 packet too short")
|
||||
})
|
||||
|
||||
t.Run("IPv6 packet too short", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{})
|
||||
_, err := conn.composeDatagram(ipv6Header[:ipv6.HeaderLen-1])
|
||||
require.ErrorContains(t, err, "connect-ip: IPv6 packet too short")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWritePacketDropsWithoutSending(t *testing.T) {
|
||||
setIHL := func(b []byte, ihl byte) []byte { b[0] = 4<<4 | ihl; return b }
|
||||
setTotalLen := func(b []byte, l uint16) []byte { binary.BigEndian.PutUint16(b[2:4], l); return b }
|
||||
payload := make([]byte, 20)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
packet []byte
|
||||
}{
|
||||
{"nil", nil},
|
||||
{"empty", []byte{}},
|
||||
{"IPv4 TTL 1", ipv4Packet(1, 17, testSrc4, testDst4, nil, payload)},
|
||||
{"IPv4 TTL 0", ipv4Packet(0, 17, testSrc4, testDst4, nil, payload)},
|
||||
{"IPv6 Hop Limit 1", ipv6Packet(1, 17, testSrc6, testDst6, payload)},
|
||||
{"IPv6 Hop Limit 0", ipv6Packet(0, 17, testSrc6, testDst6, payload)},
|
||||
{"IPv4 IHL below 5", setIHL(ipv4Packet(64, 17, testSrc4, testDst4, nil, payload), 4)},
|
||||
{"IPv4 IHL beyond total length", setIHL(ipv4Packet(64, 17, testSrc4, testDst4, nil, payload[:8]), 8)},
|
||||
{"IPv4 IHL beyond packet", setTotalLen(setIHL(ipv4Packet(64, 17, testSrc4, testDst4, nil, payload), 15), 60)},
|
||||
{"IPv4 total length beyond packet", setTotalLen(ipv4Packet(64, 17, testSrc4, testDst4, nil, payload), 41)},
|
||||
{"IPv4 total length below header", setTotalLen(ipv4Packet(64, 17, testSrc4, testDst4, nil, payload), 19)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
str := &mockStream{}
|
||||
conn := newProxiedConn(str)
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
|
||||
orig := bytes.Clone(tc.packet)
|
||||
icmpPacket, err := conn.WritePacket(tc.packet)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, icmpPacket)
|
||||
require.Empty(t, str.sent)
|
||||
require.Equal(t, orig, tc.packet, "dropped packets must not be modified")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePacketIPv4Checksum(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
options []byte
|
||||
}{
|
||||
{"no options", nil},
|
||||
{"Router Alert option", []byte{0x94, 0x04, 0x00, 0x00}},
|
||||
{"maximum header length", bytes.Repeat([]byte{0x01}, 40)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
str := &mockStream{}
|
||||
conn := newProxiedConn(str)
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
|
||||
packet := ipv4Packet(64, 17, testSrc4, testDst4, tc.options, []byte("foobar"))
|
||||
icmpPacket, err := conn.WritePacket(packet)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, icmpPacket)
|
||||
require.Len(t, str.sent, 1)
|
||||
require.Equal(t, contextIDZero, str.sent[0][:len(contextIDZero)])
|
||||
sent := str.sent[0][len(contextIDZero):]
|
||||
require.Len(t, sent, len(packet))
|
||||
require.Equal(t, uint8(63), sent[8])
|
||||
require.True(t, ipv4ChecksumValid(sent[:ipv4.HeaderLen+len(tc.options)]))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePacketTooLarge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
streamID quic.StreamID
|
||||
maxPayloadSize int
|
||||
ipv6 bool
|
||||
wantMTU int
|
||||
wantMTUTooSmall bool
|
||||
}{
|
||||
{name: "IPv4", maxPayloadSize: 1200, wantMTU: 1198},
|
||||
{name: "IPv4, 2-byte Quarter Stream ID", streamID: 4 * 64, maxPayloadSize: 1200, wantMTU: 1197},
|
||||
{name: "IPv4 minimum MTU", maxPayloadSize: 70, wantMTU: 68},
|
||||
{name: "IPv4 below minimum MTU", maxPayloadSize: 69, wantMTU: 67, wantMTUTooSmall: true},
|
||||
{name: "IPv6", maxPayloadSize: 1400, ipv6: true, wantMTU: 1398},
|
||||
{name: "IPv6, 4-byte Quarter Stream ID", streamID: 4 * 20000, maxPayloadSize: 1400, ipv6: true, wantMTU: 1395},
|
||||
{name: "IPv6 minimum MTU", maxPayloadSize: 1282, ipv6: true, wantMTU: 1280},
|
||||
{name: "IPv6 below minimum MTU", maxPayloadSize: 1281, ipv6: true, wantMTU: 1279, wantMTUTooSmall: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
str := &mockStream{streamID: tc.streamID, maxDatagramPayloadSize: tc.maxPayloadSize}
|
||||
conn := newProxiedConn(str)
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
|
||||
packetOfSize := func(size int) []byte {
|
||||
if tc.ipv6 {
|
||||
return ipv6Packet(64, 17, testSrc6, testDst6, make([]byte, size-ipv6.HeaderLen))
|
||||
}
|
||||
return ipv4Packet(64, 17, testSrc4, testDst4, nil, make([]byte, size-ipv4.HeaderLen))
|
||||
}
|
||||
require.Equal(t, tc.wantMTU, conn.MaxPacketSize())
|
||||
|
||||
icmpPacket, err := conn.WritePacket(packetOfSize(tc.wantMTU))
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, icmpPacket)
|
||||
require.Len(t, str.sent, 1)
|
||||
|
||||
icmpPacket, err = conn.WritePacket(packetOfSize(tc.wantMTU + 1))
|
||||
if tc.wantMTUTooSmall {
|
||||
require.ErrorIs(t, err, ErrMTUTooSmall)
|
||||
require.Nil(t, icmpPacket)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
if tc.ipv6 {
|
||||
msg, err := icmp.ParseMessage(ipProtoICMPv6, icmpPacket[ipv6.HeaderLen:])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ipv6.ICMPTypePacketTooBig, msg.Type)
|
||||
require.Equal(t, tc.wantMTU, msg.Body.(*icmp.PacketTooBig).MTU)
|
||||
} else {
|
||||
msg := icmpPacket[ipv4.HeaderLen:]
|
||||
require.Equal(t, []byte{3, 4}, msg[:2])
|
||||
require.Equal(t, uint16(tc.wantMTU), binary.BigEndian.Uint16(msg[6:8]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxPacketSize(t *testing.T) {
|
||||
t.Run("sends nothing", func(t *testing.T) {
|
||||
str := &mockStream{streamID: 8, maxDatagramPayloadSize: 1350}
|
||||
conn := newProxiedConn(str)
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
require.Equal(t, 1348, conn.MaxPacketSize())
|
||||
require.Empty(t, str.sent)
|
||||
})
|
||||
|
||||
t.Run("datagrams unsupported", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{sendDatagramErr: errors.New("datagram support disabled")})
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
require.Zero(t, conn.MaxPacketSize())
|
||||
})
|
||||
|
||||
t.Run("closed", func(t *testing.T) {
|
||||
conn := newProxiedConn(&mockStream{maxDatagramPayloadSize: 1350})
|
||||
require.NoError(t, conn.Close())
|
||||
require.Zero(t, conn.MaxPacketSize())
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadPacketDropsMalformedDatagrams(t *testing.T) {
|
||||
packet := ipv4Packet(64, 17, testSrc4, testDst4, nil, []byte("foobar"))
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
datagram []byte
|
||||
}{
|
||||
{"empty", []byte{}},
|
||||
{"truncated Context ID", []byte{0x40}},
|
||||
{"unknown Context ID", append([]byte{0x02}, packet...)},
|
||||
{"empty IP packet", []byte{0x00}},
|
||||
{"invalid IP packet", []byte{0x00, 0x50}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
datagrams := make(chan []byte, 2)
|
||||
conn := newProxiedConn(&mockStream{datagrams: datagrams})
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
require.NoError(t, conn.AdvertiseRoute([]IPRoute{
|
||||
{StartIP: netip.IPv4Unspecified(), EndIP: netip.MustParseAddr("255.255.255.255")},
|
||||
}))
|
||||
|
||||
datagrams <- tc.datagram
|
||||
datagrams <- append(bytes.Clone(contextIDZero), packet...)
|
||||
b := make([]byte, 1500)
|
||||
n, err := conn.ReadPacket(b)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, b[:n])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPacketShortBuffer(t *testing.T) {
|
||||
datagrams := make(chan []byte, 3)
|
||||
conn := newProxiedConn(&mockStream{datagrams: datagrams})
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
require.NoError(t, conn.AdvertiseRoute([]IPRoute{
|
||||
{StartIP: netip.IPv4Unspecified(), EndIP: netip.MustParseAddr("255.255.255.255")},
|
||||
}))
|
||||
|
||||
packet := ipv4Packet(64, 17, testSrc4, testDst4, nil, []byte("foobar"))
|
||||
for range 3 {
|
||||
datagrams <- append(bytes.Clone(contextIDZero), packet...)
|
||||
}
|
||||
n, err := conn.ReadPacket(make([]byte, len(packet)-1))
|
||||
require.ErrorIs(t, err, io.ErrShortBuffer)
|
||||
require.Zero(t, n)
|
||||
|
||||
b := make([]byte, len(packet))
|
||||
n, err = conn.ReadPacket(b)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, b[:n])
|
||||
n, err = conn.ReadPacket(make([]byte, 1500))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(packet), n)
|
||||
}
|
||||
|
||||
func TestCloseConcurrently(t *testing.T) {
|
||||
for _, side := range []string{"client", "proxy"} {
|
||||
t.Run(side, func(t *testing.T) {
|
||||
client, server := setupConns(t)
|
||||
conn := client
|
||||
if side == "proxy" {
|
||||
conn = server
|
||||
}
|
||||
|
||||
readErr := make(chan error, 1)
|
||||
go func() {
|
||||
b := make([]byte, 1500)
|
||||
for {
|
||||
if _, err := conn.ReadPacket(b); err != nil {
|
||||
readErr <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
writeErr := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
if _, err := conn.WritePacket(ipv4Packet(64, 17, testSrc4, testDst4, nil, nil)); err != nil {
|
||||
writeErr <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 4 {
|
||||
wg.Go(func() { assert.NoError(t, conn.Close()) })
|
||||
}
|
||||
wg.Wait()
|
||||
for _, errChan := range []chan error{readErr, writeErr} {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
require.ErrorIs(t, err, net.ErrClosed)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timeout")
|
||||
}
|
||||
}
|
||||
require.NoError(t, conn.Close())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4MinMTU = 68
|
||||
ipv6MinMTU = 1280
|
||||
)
|
||||
|
||||
var ErrMTUTooSmall = errors.New("connect-ip: tunnel MTU below the minimum link MTU")
|
||||
|
||||
func composeICMPTooLargePacket(b []byte, mtu int) ([]byte, error) {
|
||||
if len(b) == 0 {
|
||||
return nil, errors.New("connect-ip: empty packet")
|
||||
}
|
||||
|
||||
var icmpMessage *icmp.Message
|
||||
var psh []byte
|
||||
switch v := ipVersion(b); v {
|
||||
case 4:
|
||||
if len(b) < ipv4.HeaderLen {
|
||||
return nil, errors.New("connect-ip: IPv4 packet too short")
|
||||
}
|
||||
if mtu < ipv4MinMTU {
|
||||
return nil, fmt.Errorf("%w: %d bytes", ErrMTUTooSmall, mtu)
|
||||
}
|
||||
icmpMessage = &icmp.Message{
|
||||
Type: ipv4.ICMPTypeDestinationUnreachable,
|
||||
Code: 4,
|
||||
Body: &icmp.PacketTooBig{
|
||||
MTU: mtu,
|
||||
Data: b[:min(len(b), max(ipv4.HeaderLen, int(b[0]&0x0f)<<2)+8)],
|
||||
},
|
||||
}
|
||||
case 6:
|
||||
if len(b) < ipv6.HeaderLen {
|
||||
return nil, errors.New("connect-ip: IPv6 packet too short")
|
||||
}
|
||||
if mtu < ipv6MinMTU {
|
||||
return nil, fmt.Errorf("%w: %d bytes", ErrMTUTooSmall, mtu)
|
||||
}
|
||||
icmpMessage = &icmp.Message{
|
||||
Type: ipv6.ICMPTypePacketTooBig,
|
||||
Body: &icmp.PacketTooBig{
|
||||
MTU: mtu,
|
||||
Data: b[:min(len(b), 1232)],
|
||||
},
|
||||
}
|
||||
psh = icmp.IPv6PseudoHeader(b[24:40], b[8:24])
|
||||
default:
|
||||
return nil, fmt.Errorf("connect-ip: unknown IP version: %d", v)
|
||||
}
|
||||
|
||||
icmp, err := icmpMessage.Marshal(psh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect-ip: failed to marshal ICMP message: %w", err)
|
||||
}
|
||||
|
||||
if ipVersion(b) == 4 {
|
||||
var header [ipv4.HeaderLen]byte
|
||||
header[0] = 4<<4 | ipv4.HeaderLen>>2
|
||||
ipLen := ipv4.HeaderLen + len(icmp)
|
||||
binary.BigEndian.PutUint16(header[2:4], uint16(ipLen))
|
||||
header[8] = 64
|
||||
header[9] = 1
|
||||
copy(header[12:16], b[16:20])
|
||||
copy(header[16:20], b[12:16])
|
||||
binary.BigEndian.PutUint16(header[10:12], calculateIPv4Checksum(header[:]))
|
||||
return append(header[:], icmp...), nil
|
||||
}
|
||||
|
||||
var header [ipv6.HeaderLen]byte
|
||||
header[0] = 6 << 4
|
||||
binary.BigEndian.PutUint16(header[4:6], uint16(len(icmp)))
|
||||
header[6] = 58
|
||||
header[7] = 64
|
||||
copy(header[8:24], b[24:40])
|
||||
copy(header[24:40], b[8:24])
|
||||
return append(header[:], icmp...), nil
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
func TestICMPTooLargeIPv4(t *testing.T) {
|
||||
src := netip.MustParseAddr("192.168.1.1")
|
||||
dst := netip.MustParseAddr("8.8.8.8")
|
||||
origHdr := &ipv4.Header{
|
||||
Version: 4,
|
||||
Len: ipv4.HeaderLen,
|
||||
TotalLen: 60,
|
||||
TTL: 64,
|
||||
Protocol: 6,
|
||||
Src: src.AsSlice(),
|
||||
Dst: dst.AsSlice(),
|
||||
}
|
||||
origBytes, err := origHdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
data, err := composeICMPTooLargePacket(origBytes, 1200)
|
||||
require.NoError(t, err)
|
||||
|
||||
hdr, err := ipv4.ParseHeader(data)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, hdr.Version)
|
||||
require.Equal(t, ipProtoICMP, hdr.Protocol)
|
||||
require.Equal(t, dst.String(), hdr.Src.String())
|
||||
require.Equal(t, src.String(), hdr.Dst.String())
|
||||
require.Equal(t, uint16(hdr.Checksum), calculateIPv4Checksum(data[:ipv4.HeaderLen]))
|
||||
icmpMsg, err := icmp.ParseMessage(ipProtoICMP, data[ipv4.HeaderLen:])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ipv4.ICMPTypeDestinationUnreachable, icmpMsg.Type)
|
||||
require.Equal(t, 4, icmpMsg.Code)
|
||||
require.Equal(t, uint16(1200), binary.BigEndian.Uint16(data[ipv4.HeaderLen+6:]))
|
||||
require.Equal(t, origBytes, data[ipv4.HeaderLen+8:])
|
||||
}
|
||||
|
||||
func TestICMPTooLargeIPv4Options(t *testing.T) {
|
||||
options := []byte{0x94, 0x04, 0x00, 0x00}
|
||||
orig := ipv4Packet(64, 6, netip.MustParseAddr("192.168.1.1"), netip.MustParseAddr("8.8.8.8"), options, make([]byte, 20))
|
||||
data, err := composeICMPTooLargePacket(orig, 1200)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, orig[:ipv4.HeaderLen+len(options)+8], data[ipv4.HeaderLen+8:])
|
||||
}
|
||||
|
||||
func TestICMPTooLargeIPv6(t *testing.T) {
|
||||
const mtu = 1337
|
||||
src := netip.MustParseAddr("2001:db8::1")
|
||||
dst := netip.MustParseAddr("1:2:3:4::5")
|
||||
orig := []byte{
|
||||
0x60, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x2a,
|
||||
}
|
||||
orig = append(orig, src.AsSlice()...)
|
||||
orig = append(orig, dst.AsSlice()...)
|
||||
orig = append(orig, []byte("foobar")...)
|
||||
data, err := composeICMPTooLargePacket(orig, mtu)
|
||||
require.NoError(t, err)
|
||||
|
||||
hdr, err := ipv6.ParseHeader(data)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, hdr.Version)
|
||||
require.Equal(t, ipProtoICMPv6, hdr.NextHeader)
|
||||
require.Equal(t, dst.String(), hdr.Src.String())
|
||||
require.Equal(t, src.String(), hdr.Dst.String())
|
||||
icmpMsg, err := icmp.ParseMessage(ipProtoICMPv6, data[ipv6.HeaderLen:])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ipv6.ICMPTypePacketTooBig, icmpMsg.Type)
|
||||
icmpBody, ok := icmpMsg.Body.(*icmp.PacketTooBig)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, mtu, icmpBody.MTU)
|
||||
require.Equal(t, orig, icmpBody.Data)
|
||||
}
|
||||
|
||||
func TestICMPTooLargeMinimumMTU(t *testing.T) {
|
||||
ipv4Orig := ipv4Packet(64, 6, testSrc4, testDst4, nil, make([]byte, 100))
|
||||
ipv6Orig := ipv6Packet(64, 6, testSrc6, testDst6, make([]byte, 1300))
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
packet []byte
|
||||
mtu int
|
||||
tooSmall bool
|
||||
}{
|
||||
{"IPv4 minimum", ipv4Orig, 68, false},
|
||||
{"IPv4 below minimum", ipv4Orig, 67, true},
|
||||
{"IPv4 negative", ipv4Orig, -1, true},
|
||||
{"IPv6 minimum", ipv6Orig, 1280, false},
|
||||
{"IPv6 below minimum", ipv6Orig, 1279, true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data, err := composeICMPTooLargePacket(tc.packet, tc.mtu)
|
||||
if tc.tooSmall {
|
||||
require.ErrorIs(t, err, ErrMTUTooSmall)
|
||||
require.Nil(t, data)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestICMPFailures(t *testing.T) {
|
||||
t.Run("empty packet", func(t *testing.T) {
|
||||
_, err := composeICMPTooLargePacket([]byte{}, 1)
|
||||
require.EqualError(t, err, "connect-ip: empty packet")
|
||||
})
|
||||
|
||||
t.Run("too short IPv4 header", func(t *testing.T) {
|
||||
origHdr := &ipv4.Header{
|
||||
Version: 4,
|
||||
Len: ipv4.HeaderLen,
|
||||
TotalLen: 60,
|
||||
Src: net.IPv4(1, 2, 3, 4),
|
||||
Dst: net.IPv4(5, 6, 7, 8),
|
||||
}
|
||||
data, err := origHdr.Marshal()
|
||||
require.NoError(t, err)
|
||||
_, err = composeICMPTooLargePacket(data[:ipv4.HeaderLen-1], 1)
|
||||
require.EqualError(t, err, "connect-ip: IPv4 packet too short")
|
||||
})
|
||||
|
||||
t.Run("too short IPv6 header", func(t *testing.T) {
|
||||
data := []byte{
|
||||
0x60, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x40,
|
||||
}
|
||||
data = append(data, net.ParseIP("2001:db8::1").To16()...)
|
||||
data = append(data, net.ParseIP("2001:db8::2").To16()...)
|
||||
_, err := composeICMPTooLargePacket(data[:ipv6.HeaderLen-1], 1)
|
||||
require.EqualError(t, err, "connect-ip: IPv6 packet too short")
|
||||
})
|
||||
|
||||
t.Run("unknown IP version", func(t *testing.T) {
|
||||
data := []byte{
|
||||
0x30, 0x00, 0x00, 0x00,
|
||||
}
|
||||
_, err := composeICMPTooLargePacket(data, 1)
|
||||
require.EqualError(t, err, "connect-ip: unknown IP version: 3")
|
||||
})
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import "net/netip"
|
||||
|
||||
func rangeToPrefixes(start, end netip.Addr) []netip.Prefix {
|
||||
var prefixes []netip.Prefix
|
||||
for current := start; current.Compare(end) <= 0; {
|
||||
prefix := findLargestPrefix(current, end)
|
||||
prefixes = append(prefixes, prefix)
|
||||
|
||||
lastIP := lastIPInPrefix(prefix)
|
||||
if lastIP.Compare(end) >= 0 {
|
||||
break
|
||||
}
|
||||
current = lastIP.Next()
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
func findLargestPrefix(start, end netip.Addr) netip.Prefix {
|
||||
if start == end {
|
||||
return netip.PrefixFrom(start, start.BitLen())
|
||||
}
|
||||
|
||||
var prefixLen int
|
||||
for prefixLen = start.BitLen(); prefixLen > 0; prefixLen-- {
|
||||
prefix := netip.PrefixFrom(start, prefixLen-1)
|
||||
if lastIPInPrefix(prefix).Compare(end) > 0 || !isAligned(start, prefixLen-1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return netip.PrefixFrom(start, prefixLen)
|
||||
}
|
||||
|
||||
func lastIPInPrefix(prefix netip.Prefix) netip.Addr {
|
||||
addr := prefix.Addr()
|
||||
bits := addr.As16()
|
||||
|
||||
hostBits := addr.BitLen() - prefix.Bits()
|
||||
|
||||
for i := len(bits) - 1; i >= 0 && hostBits > 0; i-- {
|
||||
bitsInThisByte := min(8, hostBits)
|
||||
mask := byte((1 << bitsInThisByte) - 1)
|
||||
bits[i] |= mask
|
||||
hostBits -= bitsInThisByte
|
||||
}
|
||||
|
||||
if addr.Is4() {
|
||||
return netip.AddrFrom4([4]byte(bits[12:16]))
|
||||
}
|
||||
return netip.AddrFrom16(bits)
|
||||
}
|
||||
|
||||
func isAligned(addr netip.Addr, prefixLen int) bool {
|
||||
bits := addr.As16()
|
||||
|
||||
hostBits := addr.BitLen() - prefixLen
|
||||
for i := len(bits) - 1; i >= 0 && hostBits > 0; i-- {
|
||||
bitsInThisByte := min(8, hostBits)
|
||||
mask := byte((1 << bitsInThisByte) - 1)
|
||||
if bits[i]&mask != 0 {
|
||||
return false
|
||||
}
|
||||
hostBits -= bitsInThisByte
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright 2024 Marten Seemann
|
||||
* Adapted from github.com/quic-go/connect-ip-go (commit a0c35fa).
|
||||
*/
|
||||
|
||||
package connectip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIPRanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
start, end netip.Addr
|
||||
want []netip.Prefix
|
||||
}{
|
||||
{
|
||||
start: netip.MustParseAddr("192.168.1.1"),
|
||||
end: netip.MustParseAddr("192.168.1.1"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("192.168.1.1/32")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("192.168.1.0"),
|
||||
end: netip.MustParseAddr("192.168.1.1"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/31")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("192.168.1.1"),
|
||||
end: netip.MustParseAddr("192.168.1.2"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("192.168.1.1/32"), netip.MustParsePrefix("192.168.1.2/32")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("192.168.1.0"),
|
||||
end: netip.MustParseAddr("192.168.1.255"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("10.0.0.0"),
|
||||
end: netip.MustParseAddr("10.1.0.255"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/16"), netip.MustParsePrefix("10.1.0.0/24")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("2001:0db8:85a3::8a2e:0370:7334"),
|
||||
end: netip.MustParseAddr("2001:0db8:85a3::8a2e:0370:7334"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("2001:0db8:85a3::8a2e:0370:7334/128")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("2001:db8::0"),
|
||||
end: netip.MustParseAddr("2001:db8::ffff:ffff:ffff:ffff"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("2001:db8::/64")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("2001:db8::1"),
|
||||
end: netip.MustParseAddr("2001:db8::2"),
|
||||
want: []netip.Prefix{netip.MustParsePrefix("2001:db8::1/128"), netip.MustParsePrefix("2001:db8::2/128")},
|
||||
},
|
||||
{
|
||||
start: netip.MustParseAddr("2001:db8:1234:5678::"),
|
||||
end: netip.MustParseAddr("2001:db8:1234:5679::"),
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("2001:db8:1234:5678::/64"),
|
||||
netip.MustParsePrefix("2001:db8:1234:5679::/128"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s-%s", test.start, test.end), func(t *testing.T) {
|
||||
prefixes := rangeToPrefixes(test.start, test.end)
|
||||
require.Equal(t, test.want, prefixes)
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user