!feat(vless): IP restriction

Beta, only works for vless for now and it's not perfect needs a lot of testing.
This commit is contained in:
Devman 2023-06-30 13:13:36 +00:00
parent 34b68518fd
commit 3d692eb208
9 changed files with 94 additions and 22 deletions

View File

@ -42,6 +42,7 @@ type tcpWorker struct {
sniffingConfig *proxyman.SniffingConfig sniffingConfig *proxyman.SniffingConfig
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
ipLimitPool map[session.ID]*stat.UserIpRestriction
hub internet.Listener hub internet.Listener
@ -104,9 +105,18 @@ func (w *tcpWorker) callback(conn stat.Connection) {
} }
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher); err != nil { // Add this IP address to the pool for futher IP limit check
w.ipLimitPool[sid] = &stat.UserIpRestriction{
IpAddress: net.IP(conn.RemoteAddr().Network()),
}
if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher, &w.ipLimitPool, w.ipLimitPool[sid]); err != nil {
newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx)) newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx))
} }
// Deletes the IP address from the pool after the connection ends
delete(w.ipLimitPool, sid)
cancel() cancel()
conn.Close() conn.Close()
} }
@ -116,6 +126,9 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
} }
func (w *tcpWorker) Start() error { func (w *tcpWorker) Start() error {
if len(w.ipLimitPool) == 0 {
w.ipLimitPool = make(map[session.ID]*stat.UserIpRestriction)
}
ctx := context.Background() ctx := context.Background()
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) { hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) {
go w.callback(conn) go w.callback(conn)
@ -244,6 +257,7 @@ type udpWorker struct {
sniffingConfig *proxyman.SniffingConfig sniffingConfig *proxyman.SniffingConfig
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
ipLimitPool map[session.ID]*stat.UserIpRestriction
checker *task.Periodic checker *task.Periodic
activeConn map[connID]*udpConn activeConn map[connID]*udpConn
@ -326,9 +340,19 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
} }
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil {
// Add this IP address to the pool for futher IP limit check
w.ipLimitPool[sid] = &stat.UserIpRestriction{
IpAddress: net.IP(conn.RemoteAddr().Network()),
}
if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher, &w.ipLimitPool, w.ipLimitPool[sid]); err != nil {
newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx)) newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx))
} }
// Deletes the IP address from the pool after the connection ends
delete(w.ipLimitPool, sid)
conn.Close() conn.Close()
// conn not removed by checker TODO may be lock worker here is better // conn not removed by checker TODO may be lock worker here is better
if !conn.inactive { if !conn.inactive {
@ -379,6 +403,9 @@ func (w *udpWorker) clean() error {
} }
func (w *udpWorker) Start() error { func (w *udpWorker) Start() error {
if len(w.ipLimitPool) == 0 {
w.ipLimitPool = make(map[session.ID]*stat.UserIpRestriction)
}
w.activeConn = make(map[connID]*udpConn, 16) w.activeConn = make(map[connID]*udpConn, 16)
ctx := context.Background() ctx := context.Background()
h, err := udp.ListenUDP(ctx, w.address, w.port, w.stream, udp.HubCapacity(256)) h, err := udp.ListenUDP(ctx, w.address, w.port, w.stream, udp.HubCapacity(256))
@ -478,7 +505,7 @@ func (w *dsWorker) callback(conn stat.Connection) {
} }
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher); err != nil { if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher, nil, nil); err != nil {
newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx)) newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx))
} }
cancel() cancel()

View File

@ -27,6 +27,7 @@ func (u *User) ToMemoryUser() (*MemoryUser, error) {
Account: account, Account: account,
Email: u.Email, Email: u.Email,
Level: u.Level, Level: u.Level,
IpLimit: u.Ips,
}, nil }, nil
} }
@ -36,4 +37,5 @@ type MemoryUser struct {
Account Account Account Account
Email string Email string
Level uint32 Level uint32
IpLimit uint32
} }

View File

@ -32,6 +32,8 @@ type User struct {
// Protocol specific account information. Must be the account proto in one of // Protocol specific account information. Must be the account proto in one of
// the proxies. // the proxies.
Account *serial.TypedMessage `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"` Account *serial.TypedMessage `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"`
// Allowed IPs
Ips uint32 `protobuf:"varint,4,opt,name=ips,proto3" json:"ips,omitempty"`
} }
func (x *User) Reset() { func (x *User) Reset() {
@ -87,6 +89,13 @@ func (x *User) GetAccount() *serial.TypedMessage {
return nil return nil
} }
func (x *User) GetIps() uint32 {
if x != nil {
return x.Ips
}
return 0
}
var File_common_protocol_user_proto protoreflect.FileDescriptor var File_common_protocol_user_proto protoreflect.FileDescriptor
var file_common_protocol_user_proto_rawDesc = []byte{ var file_common_protocol_user_proto_rawDesc = []byte{
@ -95,20 +104,21 @@ var file_common_protocol_user_proto_rawDesc = []byte{
0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
0x6f, 0x6c, 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6f, 0x6c, 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x69, 0x61,
0x6c, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x6c, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x80, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x14,
0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x65, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c,
0x76, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20,
0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x3a, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x3a, 0x0a, 0x07, 0x61, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72,
0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c,
0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x61, 0x63, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x61,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x5e, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x04, 0x20,
0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x70, 0x73, 0x42, 0x5e, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e,
0x6c, 0x50, 0x01, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x63, 0x6f, 0x63, 0x6f, 0x6c, 0x50, 0x01, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0xaa, 0x02, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72,
0x14, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x65, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x6c, 0xaa, 0x02, 0x14, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (

View File

@ -16,4 +16,7 @@ message User {
// Protocol specific account information. Must be the account proto in one of // Protocol specific account information. Must be the account proto in one of
// the proxies. // the proxies.
xray.common.serial.TypedMessage account = 3; xray.common.serial.TypedMessage account = 3;
// Allowed IPs
uint32 ips = 4;
} }

View File

@ -233,11 +233,13 @@ func (list *PortList) UnmarshalJSON(data []byte) error {
type User struct { type User struct {
EmailString string `json:"email"` EmailString string `json:"email"`
LevelByte byte `json:"level"` LevelByte byte `json:"level"`
IpLimitByte byte `json:"ips"`
} }
func (v *User) Build() *protocol.User { func (v *User) Build() *protocol.User {
return &protocol.User{ return &protocol.User{
Email: v.EmailString, Email: v.EmailString,
Level: uint32(v.LevelByte), Level: uint32(v.LevelByte),
Ips: uint32(v.IpLimitByte),
} }
} }

View File

@ -76,7 +76,7 @@ type hasHandshakeAddress interface {
} }
// Process implements proxy.Inbound. // Process implements proxy.Inbound.
func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher, _ *map[session.ID]*stat.UserIpRestriction, _ *stat.UserIpRestriction) error {
newError("processing connection from: ", conn.RemoteAddr()).AtDebug().WriteToLog(session.ExportIDToError(ctx)) newError("processing connection from: ", conn.RemoteAddr()).AtDebug().WriteToLog(session.ExportIDToError(ctx))
dest := net.Destination{ dest := net.Destination{
Network: network, Network: network,

View File

@ -10,6 +10,7 @@ import (
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
@ -22,7 +23,7 @@ type Inbound interface {
Network() []net.Network Network() []net.Network
// Process processes a connection of given network. If necessary, the Inbound can dispatch the connection to an Outbound. // Process processes a connection of given network. If necessary, the Inbound can dispatch the connection to an Outbound.
Process(context.Context, net.Network, stat.Connection, routing.Dispatcher) error Process(context.Context, net.Network, stat.Connection, routing.Dispatcher, *map[session.ID]*stat.UserIpRestriction, *stat.UserIpRestriction) error
} }
// An Outbound process outbound connections. // An Outbound process outbound connections.

View File

@ -178,7 +178,7 @@ func (*Handler) Network() []net.Network {
} }
// Process implements proxy.Inbound.Process(). // Process implements proxy.Inbound.Process().
func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher, usrIpRstrct *map[session.ID]*stat.UserIpRestriction, connIp *stat.UserIpRestriction) error {
sid := session.ExportIDToError(ctx) sid := session.ExportIDToError(ctx)
iConn := connection iConn := connection
@ -447,6 +447,27 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
// Flow: requestAddons.Flow, // Flow: requestAddons.Flow,
} }
if (request.User.IpLimit > 0) {
addr := connection.RemoteAddr().(*net.TCPAddr)
user := account.ID.String()
uniqueIps := make(map[string]bool)
// Iterate through the connections and find unique used IP addresses withing last 30 seconds.
for _, conn := range *usrIpRstrct {
if conn.User == user && !conn.IpAddress.Equal(addr.IP) && ((time.Now().Unix() - conn.Time) < 30) {
uniqueIps[conn.IpAddress.String()] = true
}
}
if (len(uniqueIps) >= int(request.User.IpLimit)) {
return newError("User ", user, " has exceeded their allowed IPs.").AtWarning()
}
connIp.IpAddress = addr.IP
connIp.User = user
connIp.Time = time.Now().Unix()
}
var netConn net.Conn var netConn net.Conn
var rawConn syscall.RawConn var rawConn syscall.RawConn
var input *bytes.Reader var input *bytes.Reader

View File

@ -6,6 +6,12 @@ import (
"github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/features/stats"
) )
type UserIpRestriction struct {
User string
IpAddress net.IP
Time int64
}
type Connection interface { type Connection interface {
net.Conn net.Conn
} }