diff --git a/app/proxyman/inbound/worker.go b/app/proxyman/inbound/worker.go index 8ed4090a..8ba87bfa 100644 --- a/app/proxyman/inbound/worker.go +++ b/app/proxyman/inbound/worker.go @@ -42,6 +42,7 @@ type tcpWorker struct { sniffingConfig *proxyman.SniffingConfig uplinkCounter stats.Counter downlinkCounter stats.Counter + ipLimitPool map[session.ID]*stat.UserIpRestriction hub internet.Listener @@ -104,9 +105,18 @@ func (w *tcpWorker) callback(conn stat.Connection) { } 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)) } + + // Deletes the IP address from the pool after the connection ends + delete(w.ipLimitPool, sid) + cancel() conn.Close() } @@ -116,6 +126,9 @@ func (w *tcpWorker) Proxy() proxy.Inbound { } func (w *tcpWorker) Start() error { + if len(w.ipLimitPool) == 0 { + w.ipLimitPool = make(map[session.ID]*stat.UserIpRestriction) + } ctx := context.Background() hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) { go w.callback(conn) @@ -244,6 +257,7 @@ type udpWorker struct { sniffingConfig *proxyman.SniffingConfig uplinkCounter stats.Counter downlinkCounter stats.Counter + ipLimitPool map[session.ID]*stat.UserIpRestriction checker *task.Periodic 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 } 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)) } + + // Deletes the IP address from the pool after the connection ends + delete(w.ipLimitPool, sid) + conn.Close() // conn not removed by checker TODO may be lock worker here is better if !conn.inactive { @@ -379,6 +403,9 @@ func (w *udpWorker) clean() 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) ctx := context.Background() 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) - 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)) } cancel() diff --git a/common/protocol/user.go b/common/protocol/user.go index 8325f555..9006a4a0 100644 --- a/common/protocol/user.go +++ b/common/protocol/user.go @@ -27,6 +27,7 @@ func (u *User) ToMemoryUser() (*MemoryUser, error) { Account: account, Email: u.Email, Level: u.Level, + IpLimit: u.Ips, }, nil } @@ -36,4 +37,5 @@ type MemoryUser struct { Account Account Email string Level uint32 + IpLimit uint32 } diff --git a/common/protocol/user.pb.go b/common/protocol/user.pb.go index 6f063e73..2e22e41f 100644 --- a/common/protocol/user.pb.go +++ b/common/protocol/user.pb.go @@ -32,6 +32,8 @@ type User struct { // Protocol specific account information. Must be the account proto in one of // the proxies. 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() { @@ -87,6 +89,13 @@ func (x *User) GetAccount() *serial.TypedMessage { 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_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, 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, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x14, 0x0a, - 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x3a, 0x0a, 0x07, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, - 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x5e, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x50, 0x01, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 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, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x80, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x14, + 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x3a, 0x0a, 0x07, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x70, 0x73, 0x42, 0x5e, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x50, 0x01, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, + 0x65, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 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 ( diff --git a/common/protocol/user.proto b/common/protocol/user.proto index 44770edf..3632eeaa 100644 --- a/common/protocol/user.proto +++ b/common/protocol/user.proto @@ -16,4 +16,7 @@ message User { // Protocol specific account information. Must be the account proto in one of // the proxies. xray.common.serial.TypedMessage account = 3; + + // Allowed IPs + uint32 ips = 4; } diff --git a/infra/conf/common.go b/infra/conf/common.go index f8f56056..ba5abb9d 100644 --- a/infra/conf/common.go +++ b/infra/conf/common.go @@ -231,13 +231,15 @@ func (list *PortList) UnmarshalJSON(data []byte) error { } type User struct { - EmailString string `json:"email"` - LevelByte byte `json:"level"` + EmailString string `json:"email"` + LevelByte byte `json:"level"` + IpLimitByte byte `json:"ips"` } func (v *User) Build() *protocol.User { return &protocol.User{ Email: v.EmailString, Level: uint32(v.LevelByte), + Ips: uint32(v.IpLimitByte), } } diff --git a/proxy/dokodemo/dokodemo.go b/proxy/dokodemo/dokodemo.go index 42d8256f..99bb2534 100644 --- a/proxy/dokodemo/dokodemo.go +++ b/proxy/dokodemo/dokodemo.go @@ -76,7 +76,7 @@ type hasHandshakeAddress interface { } // 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)) dest := net.Destination{ Network: network, diff --git a/proxy/proxy.go b/proxy/proxy.go index fb52605c..3600a670 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -10,6 +10,7 @@ import ( "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/features/routing" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" @@ -22,7 +23,7 @@ type Inbound interface { Network() []net.Network // 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. diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 8653e1e3..00b553f8 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -178,7 +178,7 @@ func (*Handler) Network() []net.Network { } // 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) iConn := connection @@ -447,6 +447,27 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s // 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 rawConn syscall.RawConn var input *bytes.Reader diff --git a/transport/internet/stat/connection.go b/transport/internet/stat/connection.go index 6921943d..4a3fd681 100644 --- a/transport/internet/stat/connection.go +++ b/transport/internet/stat/connection.go @@ -6,6 +6,12 @@ import ( "github.com/xtls/xray-core/features/stats" ) +type UserIpRestriction struct { + User string + IpAddress net.IP + Time int64 +} + type Connection interface { net.Conn }