change project structure, performance optimization

This commit is contained in:
yuzuki999
2022-08-12 18:02:06 +08:00
parent 4bd389aa05
commit ba1e088afb
51 changed files with 181 additions and 190 deletions

View File

@@ -1,2 +1,2 @@
// Package app contains the third-party app used to replace the default app in xray-core
// Package app contains feature implementations of XrayR.
package app

View File

@@ -1,205 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.19.4
// source: app/mydispatcher/config.proto
package dispatcher
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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 SessionConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SessionConfig) Reset() {
*x = SessionConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_app_mydispatcher_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SessionConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SessionConfig) ProtoMessage() {}
func (x *SessionConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_mydispatcher_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SessionConfig.ProtoReflect.Descriptor instead.
func (*SessionConfig) Descriptor() ([]byte, []int) {
return file_app_mydispatcher_config_proto_rawDescGZIP(), []int{0}
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Settings *SessionConfig `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_mydispatcher_config_proto_msgTypes[1]
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_app_mydispatcher_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && 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_app_mydispatcher_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetSettings() *SessionConfig {
if x != nil {
return x.Settings
}
return nil
}
var File_app_mydispatcher_config_proto protoreflect.FileDescriptor
var file_app_mydispatcher_config_proto_rawDesc = []byte{
0x0a, 0x1d, 0x61, 0x70, 0x70, 0x2f, 0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68,
0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x16, 0x78, 0x72, 0x61, 0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64, 0x69, 0x73,
0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x4b,
0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x41, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74,
0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63,
0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x67, 0x0a, 0x1a, 0x63,
0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64,
0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x58, 0x72, 0x61, 0x79, 0x52, 0x2d, 0x70, 0x72,
0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x58, 0x72, 0x61, 0x79, 0x52, 0x2f, 0x61, 0x70, 0x70, 0x2f,
0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0xaa, 0x02, 0x15, 0x58,
0x72, 0x61, 0x79, 0x52, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4d, 0x79, 0x69, 0x73, 0x70, 0x61, 0x74,
0x63, 0x68, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_mydispatcher_config_proto_rawDescOnce sync.Once
file_app_mydispatcher_config_proto_rawDescData = file_app_mydispatcher_config_proto_rawDesc
)
func file_app_mydispatcher_config_proto_rawDescGZIP() []byte {
file_app_mydispatcher_config_proto_rawDescOnce.Do(func() {
file_app_mydispatcher_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_mydispatcher_config_proto_rawDescData)
})
return file_app_mydispatcher_config_proto_rawDescData
}
var file_app_mydispatcher_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_mydispatcher_config_proto_goTypes = []interface{}{
(*SessionConfig)(nil), // 0: xrayr.app.mydispatcher.SessionConfig
(*Config)(nil), // 1: xrayr.app.mydispatcher.Config
}
var file_app_mydispatcher_config_proto_depIdxs = []int32{
0, // 0: xrayr.app.mydispatcher.Config.settings:type_name -> xrayr.app.mydispatcher.SessionConfig
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_app_mydispatcher_config_proto_init() }
func file_app_mydispatcher_config_proto_init() {
if File_app_mydispatcher_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_mydispatcher_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SessionConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_mydispatcher_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_mydispatcher_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_mydispatcher_config_proto_goTypes,
DependencyIndexes: file_app_mydispatcher_config_proto_depIdxs,
MessageInfos: file_app_mydispatcher_config_proto_msgTypes,
}.Build()
File_app_mydispatcher_config_proto = out.File
file_app_mydispatcher_config_proto_rawDesc = nil
file_app_mydispatcher_config_proto_goTypes = nil
file_app_mydispatcher_config_proto_depIdxs = nil
}

View File

@@ -1,15 +0,0 @@
syntax = "proto3";
package xrayr.app.mydispatcher;
option csharp_namespace = "XrayR.App.Myispatcher";
option go_package = "github.com/Yuzuki616/V2bX/app/mydispatcher";
option java_package = "com.xrayr.app.mydispatcher";
option java_multiple_files = true;
message SessionConfig {
reserved 1;
}
message Config {
SessionConfig settings = 1;
}

View File

@@ -1,572 +0,0 @@
package dispatcher
//go:generate go run github.com/xtls/xray-core/common/errors/errorgen
import (
"context"
"fmt"
"github.com/Yuzuki616/V2bX/common/limiter"
"github.com/Yuzuki616/V2bX/common/rule"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"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/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
routing_session "github.com/xtls/xray-core/features/routing/session"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/pipe"
"strings"
"sync"
"time"
)
var errSniffingTimeout = newError("timeout on sniffing")
type cachedReader struct {
sync.Mutex
reader *pipe.Reader
cache buf.MultiBuffer
}
func (r *cachedReader) Cache(b *buf.Buffer) {
mb, _ := r.reader.ReadMultiBufferTimeout(time.Millisecond * 100)
r.Lock()
if !mb.IsEmpty() {
r.cache, _ = buf.MergeMulti(r.cache, mb)
}
b.Clear()
rawBytes := b.Extend(buf.Size)
n := r.cache.Copy(rawBytes)
b.Resize(0, int32(n))
r.Unlock()
}
func (r *cachedReader) readInternal() buf.MultiBuffer {
r.Lock()
defer r.Unlock()
if r.cache != nil && !r.cache.IsEmpty() {
mb := r.cache
r.cache = nil
return mb
}
return nil
}
func (r *cachedReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
mb := r.readInternal()
if mb != nil {
return mb, nil
}
return r.reader.ReadMultiBuffer()
}
func (r *cachedReader) ReadMultiBufferTimeout(timeout time.Duration) (buf.MultiBuffer, error) {
mb := r.readInternal()
if mb != nil {
return mb, nil
}
return r.reader.ReadMultiBufferTimeout(timeout)
}
func (r *cachedReader) Interrupt() {
r.Lock()
if r.cache != nil {
r.cache = buf.ReleaseMulti(r.cache)
}
r.Unlock()
r.reader.Interrupt()
}
// DefaultDispatcher is a default implementation of Dispatcher.
type DefaultDispatcher struct {
ohm outbound.Manager
router routing.Router
policy policy.Manager
stats stats.Manager
dns dns.Client
fdns dns.FakeDNSEngine
Limiter *limiter.Limiter
RuleManager *rule.Rule
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
d := new(DefaultDispatcher)
if err := core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dc dns.Client) error {
core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) {
d.fdns = fdns
})
return d.Init(config.(*Config), om, router, pm, sm, dc)
}); err != nil {
return nil, err
}
return d, nil
}))
}
// Init initializes DefaultDispatcher.
func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dns dns.Client) error {
d.ohm = om
d.router = router
d.policy = pm
d.stats = sm
d.Limiter = limiter.New()
d.RuleManager = rule.New()
d.dns = dns
return nil
}
// Type implements common.HasType.
func (*DefaultDispatcher) Type() interface{} {
return routing.DispatcherType()
}
// Start implements common.Runnable.
func (*DefaultDispatcher) Start() error {
return nil
}
// Close implements common.Closable.
func (*DefaultDispatcher) Close() error { return nil }
func (d *DefaultDispatcher) getLink(ctx context.Context, network net.Network, sniffing session.SniffingRequest) (*transport.Link, *transport.Link, error) {
downOpt := pipe.OptionsFromContext(ctx)
upOpt := downOpt
if network == net.Network_UDP {
var ip2domain *sync.Map // net.IP.String() => domain, this map is used by server side when client turn on fakedns
// Client will send domain address in the buffer.UDP.Address, server record all possible target IP addrs.
// When target replies, server will restore the domain and send back to client.
// Note: this map is not global but per connection context
upOpt = append(upOpt, pipe.OnTransmission(func(mb buf.MultiBuffer) buf.MultiBuffer {
for i, buffer := range mb {
if buffer.UDP == nil {
continue
}
addr := buffer.UDP.Address
if addr.Family().IsIP() {
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && fkr0.IsIPInIPPool(addr) && sniffing.Enabled {
domain := fkr0.GetDomainFromFakeDNS(addr)
if len(domain) > 0 {
buffer.UDP.Address = net.DomainAddress(domain)
newError("[fakedns client] override with domain: ", domain, " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
} else {
newError("[fakedns client] failed to find domain! :", addr.String(), " for xUDP buffer at ", i).AtWarning().WriteToLog(session.ExportIDToError(ctx))
}
}
} else {
if ip2domain == nil {
ip2domain = new(sync.Map)
newError("[fakedns client] create a new map").WriteToLog(session.ExportIDToError(ctx))
}
domain := addr.Domain()
ips, err := d.dns.LookupIP(domain, dns.IPOption{true, true, false})
if err == nil {
for _, ip := range ips {
ip2domain.Store(ip.String(), domain)
}
newError("[fakedns client] candidate ip: "+fmt.Sprintf("%v", ips), " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
} else {
newError("[fakedns client] failed to look up IP for ", domain, " for xUDP buffer at ", i).Base(err).WriteToLog(session.ExportIDToError(ctx))
}
}
}
return mb
}))
downOpt = append(downOpt, pipe.OnTransmission(func(mb buf.MultiBuffer) buf.MultiBuffer {
for i, buffer := range mb {
if buffer.UDP == nil {
continue
}
addr := buffer.UDP.Address
if addr.Family().IsIP() {
if ip2domain == nil {
continue
}
if domain, found := ip2domain.Load(addr.IP().String()); found {
buffer.UDP.Address = net.DomainAddress(domain.(string))
newError("[fakedns client] restore domain: ", domain.(string), " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
}
} else {
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok {
fakeIp := fkr0.GetFakeIPForDomain(addr.Domain())
buffer.UDP.Address = fakeIp[0]
newError("[fakedns client] restore FakeIP: ", buffer.UDP, fmt.Sprintf("%v", fakeIp), " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
}
}
}
return mb
}))
}
uplinkReader, uplinkWriter := pipe.New(upOpt...)
downlinkReader, downlinkWriter := pipe.New(downOpt...)
inboundLink := &transport.Link{
Reader: downlinkReader,
Writer: uplinkWriter,
}
outboundLink := &transport.Link{
Reader: uplinkReader,
Writer: downlinkWriter,
}
sessionInbound := session.InboundFromContext(ctx)
var user *protocol.MemoryUser
if sessionInbound != nil {
user = sessionInbound.User
}
if user != nil && len(user.Email) > 0 {
// Speed Limit and Device Limit
bucket, ok, reject := d.Limiter.GetUserBucket(sessionInbound.Tag, user.Email, sessionInbound.Source.Address.IP().String())
if reject {
newError("Devices reach the limit: ", user.Email).AtError().WriteToLog()
common.Close(outboundLink.Writer)
common.Close(inboundLink.Writer)
common.Interrupt(outboundLink.Reader)
common.Interrupt(inboundLink.Reader)
return nil, nil, newError("Devices reach the limit: ", user.Email)
}
if ok {
inboundLink.Writer = d.Limiter.RateWriter(inboundLink.Writer, bucket)
outboundLink.Writer = d.Limiter.RateWriter(outboundLink.Writer, bucket)
}
p := d.policy.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
inboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: inboundLink.Writer,
}
}
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
outboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: outboundLink.Writer,
}
}
}
}
return inboundLink, outboundLink, nil
}
func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool {
domain := result.Domain()
for _, d := range request.ExcludeForDomain {
if strings.ToLower(domain) == d {
return false
}
}
protocolString := result.Protocol()
if resComp, ok := result.(SnifferResultComposite); ok {
protocolString = resComp.ProtocolForDomainResult()
}
for _, p := range request.OverrideDestinationForProtocol {
if strings.HasPrefix(protocolString, p) {
return true
}
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && protocolString != "bittorrent" && p == "fakedns" &&
destination.Address.Family().IsIP() && fkr0.IsIPInIPPool(destination.Address) {
newError("Using sniffer ", protocolString, " since the fake DNS missed").WriteToLog(session.ExportIDToError(ctx))
return true
}
if resultSubset, ok := result.(SnifferIsProtoSubsetOf); ok {
if resultSubset.IsProtoSubsetOf(p) {
return true
}
}
}
return false
}
// Dispatch implements routing.Dispatcher.
func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destination) (*transport.Link, error) {
if !destination.IsValid() {
panic("Dispatcher: Invalid destination.")
}
ob := &session.Outbound{
Target: destination,
}
ctx = session.ContextWithOutbound(ctx, ob)
content := session.ContentFromContext(ctx)
if content == nil {
content = new(session.Content)
ctx = session.ContextWithContent(ctx, content)
}
sniffingRequest := content.SniffingRequest
inbound, outbound, err := d.getLink(ctx, destination.Network, sniffingRequest)
if err != nil {
return nil, err
}
switch {
case !sniffingRequest.Enabled:
go d.routedDispatch(ctx, outbound, destination, "")
case destination.Network != net.Network_TCP:
// Only metadata sniff will be used for non tcp connection
result, err := sniffer(ctx, nil, true)
if err == nil {
content.Protocol = result.Protocol()
if d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
go d.routedDispatch(ctx, outbound, destination, content.Protocol)
default:
go func() {
cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader),
}
outbound.Reader = cReader
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly)
if err == nil {
content.Protocol = result.Protocol()
}
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
d.routedDispatch(ctx, outbound, destination, content.Protocol)
}()
}
return inbound, nil
}
// DispatchLink implements routing.Dispatcher.
func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error {
if !destination.IsValid() {
return newError("Dispatcher: Invalid destination.")
}
ob := &session.Outbound{
Target: destination,
}
ctx = session.ContextWithOutbound(ctx, ob)
content := session.ContentFromContext(ctx)
if content == nil {
content = new(session.Content)
ctx = session.ContextWithContent(ctx, content)
}
sniffingRequest := content.SniffingRequest
switch {
case !sniffingRequest.Enabled:
go d.routedDispatch(ctx, outbound, destination, "")
case destination.Network != net.Network_TCP:
// Only metadata sniff will be used for non tcp connection
result, err := sniffer(ctx, nil, true)
if err == nil {
content.Protocol = result.Protocol()
if d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
go d.routedDispatch(ctx, outbound, destination, content.Protocol)
default:
go func() {
cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader),
}
outbound.Reader = cReader
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly)
if err == nil {
content.Protocol = result.Protocol()
}
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
d.routedDispatch(ctx, outbound, destination, content.Protocol)
}()
}
return nil
}
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (SniffResult, error) {
payload := buf.New()
defer payload.Release()
sniffer := NewSniffer(ctx)
metaresult, metadataErr := sniffer.SniffMetadata(ctx)
if metadataOnly {
return metaresult, metadataErr
}
contentResult, contentErr := func() (SniffResult, error) {
totalAttempt := 0
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
totalAttempt++
if totalAttempt > 2 {
return nil, errSniffingTimeout
}
cReader.Cache(payload)
if !payload.IsEmpty() {
result, err := sniffer.Sniff(ctx, payload.Bytes())
if err != common.ErrNoClue {
return result, err
}
}
if payload.IsFull() {
return nil, errUnknownContent
}
}
}
}()
if contentErr != nil && metadataErr == nil {
return metaresult, nil
}
if contentErr == nil && metadataErr == nil {
return CompositeResult(metaresult, contentResult), nil
}
return contentResult, contentErr
}
func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination, protocol string) {
ob := session.OutboundFromContext(ctx)
if hosts, ok := d.dns.(dns.HostsLookup); ok && destination.Address.Family().IsDomain() {
proxied := hosts.LookupHosts(ob.Target.String())
if proxied != nil {
ro := ob.RouteTarget == destination
destination.Address = *proxied
if ro {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
var handler outbound.Handler
// Check if domain and protocol hit the rule
sessionInbound := session.InboundFromContext(ctx)
// Whether the inbound connection contains a user
if sessionInbound.User != nil {
if d.RuleManager.ProtocolDetect(sessionInbound.Tag, protocol) {
newError(fmt.Sprintf("User %s access %s reject by protocol rule", sessionInbound.User.Email, destination.String())).AtError().WriteToLog()
newError("destination is reject by protocol rule")
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
if d.RuleManager.Detect(sessionInbound.Tag, destination.String(), sessionInbound.User.Email) {
newError(fmt.Sprintf("User %s access %s reject by rule", sessionInbound.User.Email, destination.String())).AtError().WriteToLog()
newError("destination is reject by rule")
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
}
routingLink := routing_session.AsRoutingContext(ctx)
inTag := routingLink.GetInboundTag()
isPickRoute := 0
if forcedOutboundTag := session.GetForcedOutboundTagFromContext(ctx); forcedOutboundTag != "" {
ctx = session.SetForcedOutboundTagToContext(ctx, "")
if h := d.ohm.GetHandler(forcedOutboundTag); h != nil {
isPickRoute = 1
newError("taking platform initialized detour [", forcedOutboundTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
handler = h
} else {
newError("non existing tag for platform initialized detour: ", forcedOutboundTag).AtError().WriteToLog(session.ExportIDToError(ctx))
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
} else if d.router != nil {
if route, err := d.router.PickRoute(routingLink); err == nil {
outTag := route.GetOutboundTag()
if h := d.ohm.GetHandler(outTag); h != nil {
isPickRoute = 2
newError("taking detour [", outTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
handler = h
} else {
newError("non existing outTag: ", outTag).AtWarning().WriteToLog(session.ExportIDToError(ctx))
}
} else {
newError("default route for ", destination).WriteToLog(session.ExportIDToError(ctx))
}
}
if handler == nil {
handler = d.ohm.GetHandler(inTag) // Default outbound handier tag should be as same as the inbound tag
}
// If there is no outbound with tag as same as the inbound tag
if handler == nil {
handler = d.ohm.GetDefaultHandler()
}
if handler == nil {
newError("default outbound handler not exist").WriteToLog(session.ExportIDToError(ctx))
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil {
if tag := handler.Tag(); tag != "" {
if inTag == "" {
accessMessage.Detour = tag
} else if isPickRoute == 1 {
accessMessage.Detour = inTag + " ==> " + tag
} else if isPickRoute == 2 {
accessMessage.Detour = inTag + " -> " + tag
} else {
accessMessage.Detour = inTag + " >> " + tag
}
}
log.Record(accessMessage)
}
handler.Dispatch(ctx, link)
}

View File

@@ -1,4 +0,0 @@
// Package dispather implement the rate limiter and the onlie device counter
package dispatcher
//go:generate go run github.com/xtls/xray-core/common/errors/errorgen

View File

@@ -1,118 +0,0 @@
package dispatcher
import (
"context"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns"
)
// newFakeDNSSniffer Create a Fake DNS metadata sniffer
func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) {
var fakeDNSEngine dns.FakeDNSEngine
{
fakeDNSEngineFeat := core.MustFromContext(ctx).GetFeature((*dns.FakeDNSEngine)(nil))
if fakeDNSEngineFeat != nil {
fakeDNSEngine = fakeDNSEngineFeat.(dns.FakeDNSEngine)
}
}
if fakeDNSEngine == nil {
errNotInit := newError("FakeDNSEngine is not initialized, but such a sniffer is used").AtError()
return protocolSnifferWithMetadata{}, errNotInit
}
return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
Target := session.OutboundFromContext(ctx).Target
if Target.Network == net.Network_TCP || Target.Network == net.Network_UDP {
domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(Target.Address)
if domainFromFakeDNS != "" {
newError("fake dns got domain: ", domainFromFakeDNS, " for ip: ", Target.Address.String()).WriteToLog(session.ExportIDToError(ctx))
return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil
}
}
if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil {
ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt)
if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok {
inPool := fkr0.IsIPInIPPool(Target.Address)
ipAddressInRangeValue.addressInRange = &inPool
}
}
return nil, common.ErrNoClue
}, metadataSniffer: true}, nil
}
type fakeDNSSniffResult struct {
domainName string
}
func (fakeDNSSniffResult) Protocol() string {
return "fakedns"
}
func (f fakeDNSSniffResult) Domain() string {
return f.domainName
}
type fakeDNSExtraOpts int
const ipAddressInRange fakeDNSExtraOpts = 1
type ipAddressInRangeOpt struct {
addressInRange *bool
}
type DNSThenOthersSniffResult struct {
domainName string
protocolOriginalName string
}
func (f DNSThenOthersSniffResult) IsProtoSubsetOf(protocolName string) bool {
return strings.HasPrefix(protocolName, f.protocolOriginalName)
}
func (DNSThenOthersSniffResult) Protocol() string {
return "fakedns+others"
}
func (f DNSThenOthersSniffResult) Domain() string {
return f.domainName
}
func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer protocolSnifferWithMetadata, others []protocolSnifferWithMetadata) (
protocolSnifferWithMetadata, error) { // nolint: unparam
// ctx may be used in the future
_ = ctx
return protocolSnifferWithMetadata{
protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
ipAddressInRangeValue := &ipAddressInRangeOpt{}
ctx = context.WithValue(ctx, ipAddressInRange, ipAddressInRangeValue)
result, err := fakeDNSSniffer.protocolSniffer(ctx, bytes)
if err == nil {
return result, nil
}
if ipAddressInRangeValue.addressInRange != nil {
if *ipAddressInRangeValue.addressInRange {
for _, v := range others {
if v.metadataSniffer || bytes != nil {
if result, err := v.protocolSniffer(ctx, bytes); err == nil {
return DNSThenOthersSniffResult{domainName: result.Domain(), protocolOriginalName: result.Protocol()}, nil
}
}
}
return nil, common.ErrNoClue
}
newError("ip address not in fake dns range, return as is").AtDebug().WriteToLog()
return nil, common.ErrNoClue
}
newError("fake dns sniffer did not set address in range option, assume false.").AtWarning().WriteToLog()
return nil, common.ErrNoClue
},
metadataSniffer: false,
}, nil
}

View File

@@ -1,132 +0,0 @@
package dispatcher
import (
"context"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/protocol/bittorrent"
"github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/protocol/tls"
)
type SniffResult interface {
Protocol() string
Domain() string
}
type protocolSniffer func(context.Context, []byte) (SniffResult, error)
type protocolSnifferWithMetadata struct {
protocolSniffer protocolSniffer
// A Metadata sniffer will be invoked on connection establishment only, with nil body,
// for both TCP and UDP connections
// It will not be shown as a traffic type for routing unless there is no other successful sniffing.
metadataSniffer bool
}
type Sniffer struct {
sniffer []protocolSnifferWithMetadata
}
func NewSniffer(ctx context.Context) *Sniffer {
ret := &Sniffer{
sniffer: []protocolSnifferWithMetadata{
{func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false},
{func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false},
{func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false},
},
}
if sniffer, err := newFakeDNSSniffer(ctx); err == nil {
others := ret.sniffer
ret.sniffer = append(ret.sniffer, sniffer)
fakeDNSThenOthers, err := newFakeDNSThenOthers(ctx, sniffer, others)
if err == nil {
ret.sniffer = append([]protocolSnifferWithMetadata{fakeDNSThenOthers}, ret.sniffer...)
}
}
return ret
}
var errUnknownContent = newError("unknown content")
func (s *Sniffer) Sniff(c context.Context, payload []byte) (SniffResult, error) {
var pendingSniffer []protocolSnifferWithMetadata
for _, si := range s.sniffer {
s := si.protocolSniffer
if si.metadataSniffer {
continue
}
result, err := s(c, payload)
if err == common.ErrNoClue {
pendingSniffer = append(pendingSniffer, si)
continue
}
if err == nil && result != nil {
return result, nil
}
}
if len(pendingSniffer) > 0 {
s.sniffer = pendingSniffer
return nil, common.ErrNoClue
}
return nil, errUnknownContent
}
func (s *Sniffer) SniffMetadata(c context.Context) (SniffResult, error) {
var pendingSniffer []protocolSnifferWithMetadata
for _, si := range s.sniffer {
s := si.protocolSniffer
if !si.metadataSniffer {
pendingSniffer = append(pendingSniffer, si)
continue
}
result, err := s(c, nil)
if err == common.ErrNoClue {
pendingSniffer = append(pendingSniffer, si)
continue
}
if err == nil && result != nil {
return result, nil
}
}
if len(pendingSniffer) > 0 {
s.sniffer = pendingSniffer
return nil, common.ErrNoClue
}
return nil, errUnknownContent
}
func CompositeResult(domainResult SniffResult, protocolResult SniffResult) SniffResult {
return &compositeResult{domainResult: domainResult, protocolResult: protocolResult}
}
type compositeResult struct {
domainResult SniffResult
protocolResult SniffResult
}
func (c compositeResult) Protocol() string {
return c.protocolResult.Protocol()
}
func (c compositeResult) Domain() string {
return c.domainResult.Domain()
}
func (c compositeResult) ProtocolForDomainResult() string {
return c.domainResult.Protocol()
}
type SnifferResultComposite interface {
ProtocolForDomainResult() string
}
type SnifferIsProtoSubsetOf interface {
IsProtoSubsetOf(protocolName string) bool
}

View File

@@ -1,25 +0,0 @@
package dispatcher
import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/features/stats"
)
type SizeStatWriter struct {
Counter stats.Counter
Writer buf.Writer
}
func (w *SizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
w.Counter.Add(int64(mb.Len()))
return w.Writer.WriteMultiBuffer(mb)
}
func (w *SizeStatWriter) Close() error {
return common.Close(w.Writer)
}
func (w *SizeStatWriter) Interrupt() {
common.Interrupt(w.Writer)
}

View File

@@ -1,44 +0,0 @@
package dispatcher_test
import (
"testing"
. "github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type TestCounter int64
func (c *TestCounter) Value() int64 {
return int64(*c)
}
func (c *TestCounter) Add(v int64) int64 {
x := int64(*c) + v
*c = TestCounter(x)
return x
}
func (c *TestCounter) Set(v int64) int64 {
*c = TestCounter(v)
return v
}
func TestStatsWriter(t *testing.T) {
var c TestCounter
writer := &SizeStatWriter{
Counter: &c,
Writer: buf.Discard,
}
mb := buf.MergeBytes(nil, []byte("abcd"))
common.Must(writer.WriteMultiBuffer(mb))
mb = buf.MergeBytes(nil, []byte("efg"))
common.Must(writer.WriteMultiBuffer(mb))
if c.Value() != 7 {
t.Fatal("unexpected counter value. want 7, but got ", c.Value())
}
}

View File

@@ -0,0 +1,33 @@
package cmd
import (
"crypto"
"github.com/go-acme/lego/v4/registration"
)
// Account represents a users local saved credentials.
type Account struct {
Email string `json:"email"`
Registration *registration.Resource `json:"registration"`
key crypto.PrivateKey
}
/** Implementation of the registration.User interface **/
// GetEmail returns the email address for the account.
func (a *Account) GetEmail() string {
return a.Email
}
// GetPrivateKey returns the private RSA account key.
func (a *Account) GetPrivateKey() crypto.PrivateKey {
return a.key
}
// GetRegistration returns the server registration.
func (a *Account) GetRegistration() *registration.Resource {
return a.Registration
}
/** End **/

View File

@@ -0,0 +1,243 @@
package cmd
import (
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
const (
baseAccountsRootFolderName = "accounts"
baseKeysFolderName = "keys"
accountFileName = "account.json"
)
// AccountsStorage A storage for account data.
//
// rootPath:
//
// ./.lego/accounts/
// │ └── root accounts directory
// └── "path" option
//
// rootUserPath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
// keysPath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/
// │ │ │ │ └── root keys directory
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
// accountFilePath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json
// │ │ │ │ └── account file
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
type AccountsStorage struct {
userID string
rootPath string
rootUserPath string
keysPath string
accountFilePath string
ctx *cli.Context
}
// NewAccountsStorage Creates a new AccountsStorage.
func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
// TODO: move to account struct? Currently MUST pass email.
email := getEmail(ctx)
serverURL, err := url.Parse(ctx.GlobalString("server"))
if err != nil {
log.Panic(err)
}
rootPath := filepath.Join(ctx.GlobalString("path"), baseAccountsRootFolderName)
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
accountsPath := filepath.Join(rootPath, serverPath)
rootUserPath := filepath.Join(accountsPath, email)
return &AccountsStorage{
userID: email,
rootPath: rootPath,
rootUserPath: rootUserPath,
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
accountFilePath: filepath.Join(rootUserPath, accountFileName),
ctx: ctx,
}
}
func (s *AccountsStorage) ExistsAccountFilePath() bool {
accountFile := filepath.Join(s.rootUserPath, accountFileName)
if _, err := os.Stat(accountFile); os.IsNotExist(err) {
return false
} else if err != nil {
log.Panic(err)
}
return true
}
func (s *AccountsStorage) GetRootPath() string {
return s.rootPath
}
func (s *AccountsStorage) GetRootUserPath() string {
return s.rootUserPath
}
func (s *AccountsStorage) GetUserID() string {
return s.userID
}
func (s *AccountsStorage) Save(account *Account) error {
jsonBytes, err := json.MarshalIndent(account, "", "\t")
if err != nil {
return err
}
return ioutil.WriteFile(s.accountFilePath, jsonBytes, filePerm)
}
func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
fileBytes, err := ioutil.ReadFile(s.accountFilePath)
if err != nil {
log.Panicf("Could not load file for account %s: %v", s.userID, err)
}
var account Account
err = json.Unmarshal(fileBytes, &account)
if err != nil {
log.Panicf("Could not parse file for account %s: %v", s.userID, err)
}
account.key = privateKey
if account.Registration == nil || account.Registration.Body.Status == "" {
reg, err := tryRecoverRegistration(s.ctx, privateKey)
if err != nil {
log.Panicf("Could not load account for %s. Registration is nil: %#v", s.userID, err)
}
account.Registration = reg
err = s.Save(&account)
if err != nil {
log.Panicf("Could not save account for %s. Registration is nil: %#v", s.userID, err)
}
}
return &account
}
func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey {
accKeyPath := filepath.Join(s.keysPath, s.userID+".key")
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType)
s.createKeysFolder()
privateKey, err := generatePrivateKey(accKeyPath, keyType)
if err != nil {
log.Panicf("Could not generate RSA private account key for account %s: %v", s.userID, err)
}
log.Printf("Saved key to %s", accKeyPath)
return privateKey
}
privateKey, err := loadPrivateKey(accKeyPath)
if err != nil {
log.Panicf("Could not load RSA private key from file %s: %v", accKeyPath, err)
}
return privateKey
}
func (s *AccountsStorage) createKeysFolder() {
if err := createNonExistingFolder(s.keysPath); err != nil {
log.Panicf("Could not check/create directory for account %s: %v", s.userID, err)
}
}
func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) {
privateKey, err := certcrypto.GeneratePrivateKey(keyType)
if err != nil {
return nil, err
}
certOut, err := os.Create(file)
if err != nil {
return nil, err
}
defer certOut.Close()
pemKey := certcrypto.PEMBlock(privateKey)
err = pem.Encode(certOut, pemKey)
if err != nil {
return nil, err
}
return privateKey, nil
}
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
switch keyBlock.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(keyBlock.Bytes)
}
return nil, errors.New("unknown private key type")
}
func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
// couldn't load account but got a key. Try to look the account up.
config := lego.NewConfig(&Account{key: privateKey})
config.CADirURL = ctx.GlobalString("server")
config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version)
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
reg, err := client.Registration.ResolveAccountByKey()
if err != nil {
return nil, err
}
return reg, nil
}

View File

@@ -0,0 +1,205 @@
package cmd
import (
"bytes"
"crypto/x509"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/urfave/cli"
"golang.org/x/net/idna"
)
const (
baseCertificatesFolderName = "certificates"
baseArchivesFolderName = "archives"
)
// CertificatesStorage a certificates storage.
//
// rootPath:
//
// ./.lego/certificates/
// │ └── root certificates directory
// └── "path" option
//
// archivePath:
//
// ./.lego/archives/
// │ └── archived certificates directory
// └── "path" option
//
type CertificatesStorage struct {
rootPath string
archivePath string
pem bool
filename string // Deprecated
}
// NewCertificatesStorage create a new certificates storage.
func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage {
return &CertificatesStorage{
rootPath: filepath.Join(ctx.GlobalString("path"), baseCertificatesFolderName),
archivePath: filepath.Join(ctx.GlobalString("path"), baseArchivesFolderName),
pem: ctx.GlobalBool("pem"),
filename: ctx.GlobalString("filename"),
}
}
func (s *CertificatesStorage) CreateRootFolder() {
err := createNonExistingFolder(s.rootPath)
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
}
func (s *CertificatesStorage) CreateArchiveFolder() {
err := createNonExistingFolder(s.archivePath)
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
}
func (s *CertificatesStorage) GetRootPath() string {
return s.rootPath
}
func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
domain := certRes.Domain
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
err := s.WriteFile(domain, ".crt", certRes.Certificate)
if err != nil {
log.Panicf("Unable to save Certificate for domain %s\n\t%v", domain, err)
}
if certRes.IssuerCertificate != nil {
err = s.WriteFile(domain, ".issuer.crt", certRes.IssuerCertificate)
if err != nil {
log.Panicf("Unable to save IssuerCertificate for domain %s\n\t%v", domain, err)
}
}
if certRes.PrivateKey != nil {
// if we were given a CSR, we don't know the private key
err = s.WriteFile(domain, ".key", certRes.PrivateKey)
if err != nil {
log.Panicf("Unable to save PrivateKey for domain %s\n\t%v", domain, err)
}
if s.pem {
err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
if err != nil {
log.Panicf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", domain, err)
}
}
} else if s.pem {
// we don't have the private key; can't write the .pem file
log.Panicf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", domain, err)
}
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
if err != nil {
log.Panicf("Unable to marshal CertResource for domain %s\n\t%v", domain, err)
}
err = s.WriteFile(domain, ".json", jsonBytes)
if err != nil {
log.Panicf("Unable to save CertResource for domain %s\n\t%v", domain, err)
}
}
func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource {
raw, err := s.ReadFile(domain, ".json")
if err != nil {
log.Panicf("Error while loading the meta data for domain %s\n\t%v", domain, err)
}
var resource certificate.Resource
if err = json.Unmarshal(raw, &resource); err != nil {
log.Panicf("Error while marshaling the meta data for domain %s\n\t%v", domain, err)
}
return resource
}
func (s *CertificatesStorage) ExistsFile(domain, extension string) bool {
filePath := s.GetFileName(domain, extension)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
} else if err != nil {
log.Panic(err)
}
return true
}
func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) {
return ioutil.ReadFile(s.GetFileName(domain, extension))
}
func (s *CertificatesStorage) GetFileName(domain, extension string) string {
filename := sanitizedDomain(domain) + extension
return filepath.Join(s.rootPath, filename)
}
func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {
content, err := s.ReadFile(domain, extension)
if err != nil {
return nil, err
}
// The input may be a bundle or a single certificate.
return certcrypto.ParsePEMBundle(content)
}
func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error {
var baseFileName string
if s.filename != "" {
baseFileName = s.filename
} else {
baseFileName = sanitizedDomain(domain)
}
filePath := filepath.Join(s.rootPath, baseFileName+extension)
return ioutil.WriteFile(filePath, data, filePerm)
}
func (s *CertificatesStorage) MoveToArchive(domain string) error {
matches, err := filepath.Glob(filepath.Join(s.rootPath, sanitizedDomain(domain)+".*"))
if err != nil {
return err
}
for _, oldFile := range matches {
date := strconv.FormatInt(time.Now().Unix(), 10)
filename := date + "." + filepath.Base(oldFile)
newFile := filepath.Join(s.archivePath, filename)
err = os.Rename(oldFile, newFile)
if err != nil {
return err
}
}
return nil
}
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
func sanitizedDomain(domain string) string {
safe, err := idna.ToASCII(strings.ReplaceAll(domain, "*", "_"))
if err != nil {
log.Panic(err)
}
return safe
}

14
app/legoCmd/cmd/cmd.go Normal file
View File

@@ -0,0 +1,14 @@
package cmd
import "github.com/urfave/cli"
// CreateCommands Creates all CLI commands.
func CreateCommands() []cli.Command {
return []cli.Command{
createRun(),
createRevoke(),
createRenew(),
createDNSHelp(),
createList(),
}
}

View File

@@ -0,0 +1,23 @@
package cmd
import (
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/urfave/cli"
)
func Before(ctx *cli.Context) error {
if ctx.GlobalString("path") == "" {
log.Panic("Could not determine current working directory. Please pass --path.")
}
err := createNonExistingFolder(ctx.GlobalString("path"))
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
if ctx.GlobalString("server") == "" {
log.Panic("Could not determine current working server. Please pass --server.")
}
return nil
}

View File

@@ -0,0 +1,73 @@
package cmd
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/urfave/cli"
)
func createDNSHelp() cli.Command {
return cli.Command{
Name: "dnshelp",
Usage: "Shows additional help for the '--dns' global option",
Action: dnsHelp,
Flags: []cli.Flag{
cli.StringFlag{
Name: "code, c",
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
},
},
}
}
func dnsHelp(ctx *cli.Context) error {
code := ctx.String("code")
if code == "" {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
ew := &errWriter{w: w}
ew.writeln(`Credentials for DNS providers must be passed through environment variables.`)
ew.writeln()
ew.writeln(`To display the documentation for a DNS providers:`)
ew.writeln()
ew.writeln("\t$ lego dnshelp -c code")
ew.writeln()
ew.writeln("All DNS codes:")
ew.writef("\t%s\n", allDNSCodes())
ew.writeln()
ew.writeln("More information: https://go-acme.github.io/lego/dns")
if ew.err != nil {
return ew.err
}
return w.Flush()
}
return displayDNSHelp(strings.ToLower(code))
}
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) writeln(a ...interface{}) {
if ew.err != nil {
return
}
_, ew.err = fmt.Fprintln(ew.w, a...)
}
func (ew *errWriter) writef(format string, a ...interface{}) {
if ew.err != nil {
return
}
_, ew.err = fmt.Fprintf(ew.w, format, a...)
}

136
app/legoCmd/cmd/cmd_list.go Normal file
View File

@@ -0,0 +1,136 @@
package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"path/filepath"
"strings"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/urfave/cli"
)
func createList() cli.Command {
return cli.Command{
Name: "list",
Usage: "Display certificates and accounts information.",
Action: list,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "accounts, a",
Usage: "Display accounts.",
},
cli.BoolFlag{
Name: "names, n",
Usage: "Display certificate common names only.",
},
},
}
}
func list(ctx *cli.Context) error {
if ctx.Bool("accounts") && !ctx.Bool("names") {
if err := listAccount(ctx); err != nil {
return err
}
}
return listCertificates(ctx)
}
func listCertificates(ctx *cli.Context) error {
certsStorage := NewCertificatesStorage(ctx)
matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt"))
if err != nil {
return err
}
names := ctx.Bool("names")
if len(matches) == 0 {
if !names {
fmt.Println("No certificates found.")
}
return nil
}
if !names {
fmt.Println("Found the following certs:")
}
for _, filename := range matches {
if strings.HasSuffix(filename, ".issuer.crt") {
continue
}
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
pCert, err := certcrypto.ParsePEMCertificate(data)
if err != nil {
return err
}
if names {
fmt.Println(pCert.Subject.CommonName)
} else {
fmt.Println(" Certificate Name:", pCert.Subject.CommonName)
fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", "))
fmt.Println(" Expiry Date:", pCert.NotAfter)
fmt.Println(" Certificate Path:", filename)
fmt.Println()
}
}
return nil
}
func listAccount(ctx *cli.Context) error {
// fake email, needed by NewAccountsStorage
if err := ctx.GlobalSet("email", "unknown"); err != nil {
return err
}
accountsStorage := NewAccountsStorage(ctx)
matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json"))
if err != nil {
return err
}
if len(matches) == 0 {
fmt.Println("No accounts found.")
return nil
}
fmt.Println("Found the following accounts:")
for _, filename := range matches {
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
var account Account
err = json.Unmarshal(data, &account)
if err != nil {
return err
}
uri, err := url.Parse(account.Registration.URI)
if err != nil {
return err
}
fmt.Println(" Email:", account.Email)
fmt.Println(" Server:", uri.Host)
fmt.Println(" Path:", filepath.Dir(filename))
fmt.Println()
}
return nil
}

View File

@@ -0,0 +1,225 @@
package cmd
import (
"crypto"
"crypto/x509"
"time"
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/urfave/cli"
)
const (
renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
renewEnvCertDomain = "LEGO_CERT_DOMAIN"
renewEnvCertPath = "LEGO_CERT_PATH"
renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
)
func createRenew() cli.Command {
return cli.Command{
Name: "renew",
Usage: "Renew a certificate",
Action: renew,
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.GlobalStringSlice("domains")) > 0
hasCsr := len(ctx.GlobalString("csr")) > 0
if hasDomains && hasCsr {
log.Panic("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Panic("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
return nil
},
Flags: []cli.Flag{
cli.IntFlag{
Name: "days",
Value: 30,
Usage: "The number of days left on a certificate to renew it.",
},
cli.BoolFlag{
Name: "reuse-key",
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
},
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
cli.StringFlag{
Name: "renew-hook",
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
},
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
},
}
}
func renew(ctx *cli.Context) error {
account, client := setup(ctx, NewAccountsStorage(ctx))
setupChallenges(ctx, client)
if account.Registration == nil {
log.Panicf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
}
certsStorage := NewCertificatesStorage(ctx)
bundle := !ctx.Bool("no-bundle")
meta := map[string]string{renewEnvAccountEmail: account.Email}
// CSR
if ctx.GlobalIsSet("csr") {
return renewForCSR(ctx, client, certsStorage, bundle, meta)
}
// Domains
return renewForDomains(ctx, client, certsStorage, bundle, meta)
}
func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
domains := ctx.GlobalStringSlice("domains")
domain := domains[0]
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Panicf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
if !needRenewal(cert, domain, ctx.Int("days")) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
certDomains := certcrypto.ExtractDomains(cert)
var privateKey crypto.PrivateKey
if ctx.Bool("reuse-key") {
keyBytes, errR := certsStorage.ReadFile(domain, ".key")
if errR != nil {
log.Panicf("Error while loading the private key for domain %s\n\t%v", domain, errR)
}
privateKey, errR = certcrypto.ParsePEMPrivateKey(keyBytes)
if errR != nil {
return errR
}
}
request := certificate.ObtainRequest{
Domains: merge(certDomains, domains),
Bundle: bundle,
PrivateKey: privateKey,
MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
}
certRes, err := client.Certificate.Obtain(request)
if err != nil {
log.Panic(err)
}
certsStorage.SaveResource(certRes)
meta[renewEnvCertDomain] = domain
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
return launchHook(ctx.String("renew-hook"), meta)
}
func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
csr, err := readCSRFile(ctx.GlobalString("csr"))
if err != nil {
log.Panic(err)
}
domain := csr.Subject.CommonName
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Panicf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
if !needRenewal(cert, domain, ctx.Int("days")) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
certRes, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
})
if err != nil {
log.Panic(err)
}
certsStorage.SaveResource(certRes)
meta[renewEnvCertDomain] = domain
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
return launchHook(ctx.String("renew-hook"), meta)
}
func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
if x509Cert.IsCA {
log.Panicf("[%s] Certificate bundle starts with a CA certificate", domain)
}
if days >= 0 {
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
if notAfter > days {
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
domain, notAfter, days)
return false
}
}
return true
}
func merge(prevDomains, nextDomains []string) []string {
for _, next := range nextDomains {
var found bool
for _, prev := range prevDomains {
if prev == next {
found = true
break
}
}
if !found {
prevDomains = append(prevDomains, next)
}
}
return prevDomains
}

View File

@@ -0,0 +1,118 @@
package cmd
import (
"crypto/x509"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_merge(t *testing.T) {
testCases := []struct {
desc string
prevDomains []string
nextDomains []string
expected []string
}{
{
desc: "all empty",
prevDomains: []string{},
nextDomains: []string{},
expected: []string{},
},
{
desc: "next empty",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{},
expected: []string{"a", "b", "c"},
},
{
desc: "prev empty",
prevDomains: []string{},
nextDomains: []string{"a", "b", "c"},
expected: []string{"a", "b", "c"},
},
{
desc: "merge append",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{"a", "c", "d"},
expected: []string{"a", "b", "c", "d"},
},
{
desc: "merge same",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{"a", "b", "c"},
expected: []string{"a", "b", "c"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := merge(test.prevDomains, test.nextDomains)
assert.Equal(t, test.expected, actual)
})
}
}
func Test_needRenewal(t *testing.T) {
testCases := []struct {
desc string
x509Cert *x509.Certificate
days int
expected bool
}{
{
desc: "30 days, NotAfter now",
x509Cert: &x509.Certificate{
NotAfter: time.Now(),
},
days: 30,
expected: true,
},
{
desc: "30 days, NotAfter 31 days",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(31*24*time.Hour + 1*time.Second),
},
days: 30,
expected: false,
},
{
desc: "30 days, NotAfter 30 days",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: 30,
expected: true,
},
{
desc: "0 days, NotAfter 30 days: only the day of the expiration",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: 0,
expected: false,
},
{
desc: "-1 days, NotAfter 30 days: always renew",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: -1,
expected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
actual := needRenewal(test.x509Cert, "foo.com", test.days)
assert.Equal(t, test.expected, actual)
})
}
}

View File

@@ -0,0 +1,62 @@
package cmd
import (
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/urfave/cli"
)
func createRevoke() cli.Command {
return cli.Command{
Name: "revoke",
Usage: "Revoke a certificate",
Action: revoke,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "keep, k",
Usage: "Keep the certificates after the revocation instead of archiving them.",
},
},
}
}
func revoke(ctx *cli.Context) error {
acc, client := setup(ctx, NewAccountsStorage(ctx))
if acc.Registration == nil {
log.Panicf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
}
certsStorage := NewCertificatesStorage(ctx)
certsStorage.CreateRootFolder()
for _, domain := range ctx.GlobalStringSlice("domains") {
log.Printf("Trying to revoke certificate for domain %s", domain)
certBytes, err := certsStorage.ReadFile(domain, ".crt")
if err != nil {
log.Panicf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
}
err = client.Certificate.Revoke(certBytes)
if err != nil {
log.Panicf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
}
log.Println("Certificate was revoked.")
if ctx.Bool("keep") {
return nil
}
certsStorage.CreateArchiveFolder()
err = certsStorage.MoveToArchive(domain)
if err != nil {
return err
}
log.Println("Certificate was archived for domain:", domain)
}
return nil
}

186
app/legoCmd/cmd/cmd_run.go Normal file
View File

@@ -0,0 +1,186 @@
package cmd
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
func createRun() cli.Command {
return cli.Command{
Name: "run",
Usage: "Register an account, then create and install a certificate",
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.GlobalStringSlice("domains")) > 0
hasCsr := len(ctx.GlobalString("csr")) > 0
if hasDomains && hasCsr {
log.Panic("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Panic("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
return nil
},
Action: run,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
cli.StringFlag{
Name: "run-hook",
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
},
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
},
}
}
const rootPathWarningMessage = `!!!! HEADS UP !!!!
Your account credentials have been saved in your Let's Encrypt
configuration directory at "%s".
You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.
`
func run(ctx *cli.Context) error {
accountsStorage := NewAccountsStorage(ctx)
account, client := setup(ctx, accountsStorage)
setupChallenges(ctx, client)
if account.Registration == nil {
reg, err := register(ctx, client)
if err != nil {
log.Panicf("Could not complete registration\n\t%v", err)
}
account.Registration = reg
if err = accountsStorage.Save(account); err != nil {
log.Panic(err)
}
fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath())
}
certsStorage := NewCertificatesStorage(ctx)
certsStorage.CreateRootFolder()
cert, err := obtainCertificate(ctx, client)
if err != nil {
// Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error.
// Due to us not returning partial certificate we can just exit here instead of at the end.
log.Panicf("Could not obtain certificates:\n\t%v", err)
}
certsStorage.SaveResource(cert)
meta := map[string]string{
renewEnvAccountEmail: account.Email,
renewEnvCertDomain: cert.Domain,
renewEnvCertPath: certsStorage.GetFileName(cert.Domain, ".crt"),
renewEnvCertKeyPath: certsStorage.GetFileName(cert.Domain, ".key"),
}
return launchHook(ctx.String("run-hook"), meta)
}
func handleTOS(ctx *cli.Context, client *lego.Client) bool {
// Check for a global accept override
if ctx.GlobalBool("accept-tos") {
return true
}
reader := bufio.NewReader(os.Stdin)
log.Printf("Please review the TOS at %s", client.GetToSURL())
for {
fmt.Println("Do you accept the TOS? Y/n")
text, err := reader.ReadString('\n')
if err != nil {
log.Panicf("Could not read from console: %v", err)
}
text = strings.Trim(text, "\r\n")
switch text {
case "", "y", "Y":
return true
case "n", "N":
return false
default:
fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.")
}
}
}
func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, error) {
accepted := handleTOS(ctx, client)
if !accepted {
log.Panic("You did not accept the TOS. Unable to proceed.")
}
if ctx.GlobalBool("eab") {
kid := ctx.GlobalString("kid")
hmacEncoded := ctx.GlobalString("hmac")
if kid == "" || hmacEncoded == "" {
log.Panicf("Requires arguments --kid and --hmac.")
}
return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: accepted,
Kid: kid,
HmacEncoded: hmacEncoded,
})
}
return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) {
bundle := !ctx.Bool("no-bundle")
domains := ctx.GlobalStringSlice("domains")
if len(domains) > 0 {
// obtain a certificate, generating a new private key
request := certificate.ObtainRequest{
Domains: domains,
Bundle: bundle,
MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
}
return client.Certificate.Obtain(request)
}
// read the CSR
csr, err := readCSRFile(ctx.GlobalString("csr"))
if err != nil {
return nil, err
}
// obtain a certificate for this CSR
return client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
})
}

120
app/legoCmd/cmd/flags.go Normal file
View File

@@ -0,0 +1,120 @@
package cmd
import (
"github.com/go-acme/lego/v4/lego"
"github.com/urfave/cli"
)
func CreateFlags(defaultPath string) []cli.Flag {
return []cli.Flag{
cli.StringSliceFlag{
Name: "domains, d",
Usage: "Add a domain to the process. Can be specified multiple times.",
},
cli.StringFlag{
Name: "server, s",
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
Value: lego.LEDirectoryProduction,
},
cli.BoolFlag{
Name: "accept-tos, a",
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
},
cli.StringFlag{
Name: "email, m",
Usage: "Email used for registration and recovery contact.",
},
cli.StringFlag{
Name: "csr, c",
Usage: "Certificate signing request filename, if an external CSR is to be used.",
},
cli.BoolFlag{
Name: "eab",
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
cli.StringFlag{
Name: "kid",
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
cli.StringFlag{
Name: "hmac",
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
},
cli.StringFlag{
Name: "key-type, k",
Value: "ec256",
Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384.",
},
cli.StringFlag{
Name: "filename",
Usage: "(deprecated) Filename of the generated certificate.",
},
cli.StringFlag{
Name: "path",
EnvVar: "LEGO_PATH",
Usage: "Directory to use for storing the data.",
Value: defaultPath,
},
cli.BoolFlag{
Name: "http",
Usage: "Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges.",
},
cli.StringFlag{
Name: "http.port",
Usage: "Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port.",
Value: ":80",
},
cli.StringFlag{
Name: "http.proxy-header",
Usage: "Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy.",
Value: "Host",
},
cli.StringFlag{
Name: "http.webroot",
Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge",
},
cli.StringSliceFlag{
Name: "http.memcached-host",
Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.",
},
cli.BoolFlag{
Name: "tls",
Usage: "Use the TLS challenge to solve challenges. Can be mixed with other types of challenges.",
},
cli.StringFlag{
Name: "tls.port",
Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port.",
Value: ":443",
},
cli.StringFlag{
Name: "dns",
Usage: "Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.",
},
cli.BoolFlag{
Name: "dns.disable-cp",
Usage: "By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers.",
},
cli.StringSliceFlag{
Name: "dns.resolvers",
Usage: "Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.",
},
cli.IntFlag{
Name: "http-timeout",
Usage: "Set the HTTP timeout value to a specific value in seconds.",
},
cli.IntFlag{
Name: "dns-timeout",
Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries.",
Value: 10,
},
cli.BoolFlag{
Name: "pem",
Usage: "Generate a .pem file by concatenating the .key and .crt files together.",
},
cli.IntFlag{
Name: "cert.timeout",
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
Value: 30,
},
}
}

47
app/legoCmd/cmd/hook.go Normal file
View File

@@ -0,0 +1,47 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
func launchHook(hook string, meta map[string]string) error {
if hook == "" {
return nil
}
ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
parts := strings.Fields(hook)
cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...)
output, err := cmdCtx.CombinedOutput()
if len(output) > 0 {
fmt.Println(string(output))
}
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
return err
}
func metaToEnv(meta map[string]string) []string {
var envs []string
for k, v := range meta {
envs = append(envs, k+"="+v)
}
return envs
}

129
app/legoCmd/cmd/setup.go Normal file
View File

@@ -0,0 +1,129 @@
package cmd
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
const filePerm os.FileMode = 0o600
func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) {
keyType := getKeyType(ctx)
privateKey := accountsStorage.GetPrivateKey(keyType)
var account *Account
if accountsStorage.ExistsAccountFilePath() {
account = accountsStorage.LoadAccount(privateKey)
} else {
account = &Account{Email: accountsStorage.GetUserID(), key: privateKey}
}
client := newClient(ctx, account, keyType)
return account, client
}
func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client {
config := lego.NewConfig(acc)
config.CADirURL = ctx.GlobalString("server")
config.Certificate = lego.CertificateConfig{
KeyType: keyType,
Timeout: time.Duration(ctx.GlobalInt("cert.timeout")) * time.Second,
}
config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version)
if ctx.GlobalIsSet("http-timeout") {
config.HTTPClient.Timeout = time.Duration(ctx.GlobalInt("http-timeout")) * time.Second
}
client, err := lego.NewClient(config)
if err != nil {
log.Panicf("Could not create client: %v", err)
}
if client.GetExternalAccountRequired() && !ctx.GlobalIsSet("eab") {
log.Panic("Server requires External Account Binding. Use --eab with --kid and --hmac.")
}
return client
}
// getKeyType the type from which private keys should be generated.
func getKeyType(ctx *cli.Context) certcrypto.KeyType {
keyType := ctx.GlobalString("key-type")
switch strings.ToUpper(keyType) {
case "RSA2048":
return certcrypto.RSA2048
case "RSA4096":
return certcrypto.RSA4096
case "RSA8192":
return certcrypto.RSA8192
case "EC256":
return certcrypto.EC256
case "EC384":
return certcrypto.EC384
}
log.Panicf("Unsupported KeyType: %s", keyType)
return ""
}
func getEmail(ctx *cli.Context) string {
email := ctx.GlobalString("email")
if email == "" {
log.Panic("You have to pass an account (email address) to the program using --email or -m")
}
return email
}
func createNonExistingFolder(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, 0o700)
} else if err != nil {
return err
}
return nil
}
func readCSRFile(filename string) (*x509.CertificateRequest, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
raw := bytes
// see if we can find a PEM-encoded CSR
var p *pem.Block
rest := bytes
for {
// decode a PEM block
p, rest = pem.Decode(rest)
// did we fail?
if p == nil {
break
}
// did we get a CSR?
if p.Type == "CERTIFICATE REQUEST" {
raw = p.Bytes
}
}
// no PEM-encoded CSR
// assume we were given a DER-encoded ASN.1 CSR
// (if this assumption is wrong, parsing these bytes will fail)
return x509.ParseCertificateRequest(raw)
}

View File

@@ -0,0 +1,126 @@
package cmd
import (
"net"
"strings"
"time"
"github.com/Yuzuki616/V2bX/app/legoCmd/log"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/dns"
"github.com/go-acme/lego/v4/providers/http/memcached"
"github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/urfave/cli"
)
func setupChallenges(ctx *cli.Context, client *lego.Client) {
if !ctx.GlobalBool("http") && !ctx.GlobalBool("tls") && !ctx.GlobalIsSet("dns") {
log.Panic("No challenge selected. You must specify at least one challenge: `--http`, `--tls`, `--dns`.")
}
if ctx.GlobalBool("http") {
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx))
if err != nil {
log.Panic(err)
}
}
if ctx.GlobalBool("tls") {
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx))
if err != nil {
log.Panic(err)
}
}
if ctx.GlobalIsSet("dns") {
setupDNS(ctx, client)
}
}
func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.GlobalIsSet("http.webroot"):
ps, err := webroot.NewHTTPProvider(ctx.GlobalString("http.webroot"))
if err != nil {
log.Panic(err)
}
return ps
case ctx.GlobalIsSet("http.memcached-host"):
ps, err := memcached.NewMemcachedProvider(ctx.GlobalStringSlice("http.memcached-host"))
if err != nil {
log.Panic(err)
}
return ps
case ctx.GlobalIsSet("http.port"):
iface := ctx.GlobalString("http.port")
if !strings.Contains(iface, ":") {
log.Panicf("The --http switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
if err != nil {
log.Panic(err)
}
srv := http01.NewProviderServer(host, port)
if header := ctx.GlobalString("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
case ctx.GlobalBool("http"):
srv := http01.NewProviderServer("", "")
if header := ctx.GlobalString("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
default:
log.Panic("Invalid HTTP challenge options.")
return nil
}
}
func setupTLSProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.GlobalIsSet("tls.port"):
iface := ctx.GlobalString("tls.port")
if !strings.Contains(iface, ":") {
log.Panicf("The --tls switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
if err != nil {
log.Panic(err)
}
return tlsalpn01.NewProviderServer(host, port)
case ctx.GlobalBool("tls"):
return tlsalpn01.NewProviderServer("", "")
default:
log.Panic("Invalid HTTP challenge options.")
return nil
}
}
func setupDNS(ctx *cli.Context, client *lego.Client) {
provider, err := dns.NewDNSChallengeProviderByName(ctx.GlobalString("dns"))
if err != nil {
log.Panic(err)
}
servers := ctx.GlobalStringSlice("dns.resolvers")
err = client.Challenge.SetDNS01Provider(provider,
dns01.CondOption(len(servers) > 0,
dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.GlobalStringSlice("dns.resolvers")))),
dns01.CondOption(ctx.GlobalBool("dns.disable-cp"),
dns01.DisableCompletePropagationRequirement()),
dns01.CondOption(ctx.GlobalIsSet("dns-timeout"),
dns01.AddDNSTimeout(time.Duration(ctx.GlobalInt("dns-timeout"))*time.Second)),
)
if err != nil {
log.Panic(err)
}
}

File diff suppressed because it is too large Load Diff

189
app/legoCmd/lego.go Normal file
View File

@@ -0,0 +1,189 @@
// Package legocmd Let's Encrypt client to go!
// CLI application for generating Let's Encrypt certificates using the ACME package.
package legoCmd
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/Yuzuki616/V2bX/app/legoCmd/cmd"
"github.com/urfave/cli"
)
var version = "dev"
var defaultPath string
type LegoCMD struct {
cmdClient *cli.App
}
func New() (*LegoCMD, error) {
app := cli.NewApp()
app.Name = "lego"
app.HelpName = "lego"
app.Usage = "Let's Encrypt client written in Go"
app.EnableBashCompletion = true
app.Version = version
cli.VersionPrinter = func(c *cli.Context) {
fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH)
}
// Set default pathTemp to configPath/cert
var pathTemp = ""
configPath := os.Getenv("XRAY_LOCATION_CONFIG")
if configPath != "" {
pathTemp = configPath
} else if cwd, err := os.Getwd(); err == nil {
pathTemp = cwd
} else {
pathTemp = "."
}
defaultPath = filepath.Join(pathTemp, "cert")
app.Flags = cmd.CreateFlags(defaultPath)
app.Before = cmd.Before
app.Commands = cmd.CreateCommands()
lego := &LegoCMD{
cmdClient: app,
}
return lego, nil
}
// DNSCert cert a domain using DNS API
func (l *LegoCMD) DNSCert(domain, email, provider string, DNSEnv map[string]string) (CertPath string, KeyPath string, err error) {
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknow panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
// Set Env for DNS configuration
for key, value := range DNSEnv {
os.Setenv(key, value)
}
// First check if the certificate exists
CertPath, KeyPath, err = checkCertfile(domain)
if err == nil {
return CertPath, KeyPath, err
}
argstring := fmt.Sprintf("lego -a -d %s -m %s --dns %s run", domain, email, provider)
err = l.cmdClient.Run(strings.Split(argstring, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
// HTTPCert cert a domain using http methods
func (l *LegoCMD) HTTPCert(domain, email string) (CertPath string, KeyPath string, err error) {
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknow panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
// First check if the certificate exists
CertPath, KeyPath, err = checkCertfile(domain)
if err == nil {
return CertPath, KeyPath, err
}
argString := fmt.Sprintf("lego -a -d %s -m %s --http run", domain, email)
err = l.cmdClient.Run(strings.Split(argString, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
//RenewCert renew a domain cert
func (l *LegoCMD) RenewCert(domain, email, certMode, provider string, DNSEnv map[string]string) (CertPath string, KeyPath string, err error) {
var argstring string
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknown panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
if certMode == "http" {
argstring = fmt.Sprintf("lego -a -d %s -m %s --http renew --days 30", domain, email)
} else if certMode == "dns" {
// Set Env for DNS configuration
for key, value := range DNSEnv {
os.Setenv(key, value)
}
argstring = fmt.Sprintf("lego -a -d %s -m %s --dns %s renew --days 30", domain, email, provider)
} else {
return "", "", fmt.Errorf("unsupport cert mode: %s", certMode)
}
err = l.cmdClient.Run(strings.Split(argstring, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
func checkCertfile(domain string) (string, string, error) {
keyPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.key", domain))
certPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.crt", domain))
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
return "", "", fmt.Errorf("cert key failed: %s", domain)
}
if _, err := os.Stat(certPath); os.IsNotExist(err) {
return "", "", fmt.Errorf("cert cert failed: %s", domain)
}
absKeyPath, _ := filepath.Abs(keyPath)
absCertPath, _ := filepath.Abs(certPath)
return absCertPath, absKeyPath, nil
}

82
app/legoCmd/lego_test.go Normal file
View File

@@ -0,0 +1,82 @@
package legoCmd_test
import (
"testing"
"github.com/Yuzuki616/V2bX/app/legoCmd"
)
func TestLegoClient(t *testing.T) {
_, err := legoCmd.New()
if err != nil {
t.Error(err)
}
}
func TestLegoDNSCert(t *testing.T) {
lego, err := legoCmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
provider string = "alidns"
DNSEnv map[string]string
)
DNSEnv = make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certPath, keyPath, err := lego.DNSCert(domain, email, provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}
func TestLegoHTTPCert(t *testing.T) {
lego, err := legoCmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
)
certPath, keyPath, err := lego.HTTPCert(domain, email)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}
func TestLegoRenewCert(t *testing.T) {
lego, err := legoCmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
provider string = "alidns"
DNSEnv map[string]string
)
DNSEnv = make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certPath, keyPath, err := lego.RenewCert(domain, email, "dns", provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
certPath, keyPath, err = lego.RenewCert(domain, email, "http", provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}

59
app/legoCmd/log/log.go Normal file
View File

@@ -0,0 +1,59 @@
package log
import (
"log"
"os"
)
// Logger is an optional custom logger.
var Logger StdLogger = log.New(os.Stdout, "", log.LstdFlags)
// StdLogger interface for Standard Logger.
type StdLogger interface {
Panic(args ...interface{})
Fatalln(args ...interface{})
Panicf(format string, args ...interface{})
Print(args ...interface{})
Println(args ...interface{})
Printf(format string, args ...interface{})
}
// Panic writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Panic(args ...interface{}) {
Logger.Panic(args...)
}
// Panicf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Panicf(format string, args ...interface{}) {
Logger.Panicf(format, args...)
}
// Print writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Print(args ...interface{}) {
Logger.Print(args...)
}
// Println writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Println(args ...interface{}) {
Logger.Println(args...)
}
// Printf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Printf(format string, args ...interface{}) {
Logger.Printf(format, args...)
}
// Warnf writes a log entry.
func Warnf(format string, args ...interface{}) {
Printf("[WARN] "+format, args...)
}
// Infof writes a log entry.
func Infof(format string, args ...interface{}) {
Printf("[INFO] "+format, args...)
}

View File

@@ -1,4 +1,4 @@
package dispatcher
package limiter
import "github.com/xtls/xray-core/common/errors"

233
app/limiter/limiter.go Normal file
View File

@@ -0,0 +1,233 @@
// Package limiter is to control the links that go into the dispather
package limiter
import (
"fmt"
"sync"
"time"
"github.com/Yuzuki616/V2bX/api"
"github.com/juju/ratelimit"
)
type UserInfo struct {
UID int
SpeedLimit uint64
DeviceLimit int
}
type InboundInfo struct {
Tag string
NodeSpeedLimit uint64
UserInfo *sync.Map // Key: Uid value: UserInfo
BucketHub *sync.Map // key: Uid, value: *ratelimit.Bucket
UserOnlineIP *sync.Map // Key: Uid Value: *sync.Map: Key: IP, Value: bool
}
type Limiter struct {
InboundInfo *sync.Map // Key: Tag, Value: *InboundInfo
}
func New() *Limiter {
return &Limiter{
InboundInfo: new(sync.Map),
}
}
func (l *Limiter) AddInboundLimiter(tag string, nodeInfo *api.NodeInfo, userList []api.UserInfo) error {
inboundInfo := &InboundInfo{
Tag: tag,
NodeSpeedLimit: nodeInfo.SpeedLimit,
BucketHub: new(sync.Map),
UserOnlineIP: new(sync.Map),
}
userMap := new(sync.Map)
for i := range userList {
/*if (*userList)[i].SpeedLimit == 0 {
(*userList)[i].SpeedLimit = nodeInfo.SpeedLimit
}
if (*userList)[i].DeviceLimit == 0 {
(*userList)[i].DeviceLimit = nodeInfo.DeviceLimit
}*/
userMap.Store(fmt.Sprintf("%s|%s|%d", tag, (userList)[i].V2rayUser.Email, (userList)[i].UID),
UserInfo{
UID: (userList)[i].UID,
SpeedLimit: nodeInfo.SpeedLimit,
DeviceLimit: nodeInfo.DeviceLimit,
})
}
inboundInfo.UserInfo = userMap
l.InboundInfo.Store(tag, inboundInfo) // Replace the old inbound info
return nil
}
func (l *Limiter) UpdateInboundLimiter(tag string, nodeInfo *api.NodeInfo, updatedUserList []api.UserInfo) error {
if value, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := value.(*InboundInfo)
// Update User info
for i := range updatedUserList {
inboundInfo.UserInfo.Store(fmt.Sprintf("%s|%s|%d", tag,
(updatedUserList)[i].V2rayUser.Email, (updatedUserList)[i].UID), UserInfo{
UID: (updatedUserList)[i].UID,
SpeedLimit: nodeInfo.SpeedLimit,
DeviceLimit: nodeInfo.DeviceLimit,
})
inboundInfo.BucketHub.Delete(fmt.Sprintf("%s|%s|%d", tag,
(updatedUserList)[i].V2rayUser.Email, (updatedUserList)[i].UID)) // Delete old limiter bucket
}
} else {
return fmt.Errorf("no such inbound in limiter: %s", tag)
}
return nil
}
func (l *Limiter) DeleteInboundLimiter(tag string) error {
l.InboundInfo.Delete(tag)
return nil
}
type UserIp struct {
Uid int `json:"Uid"`
IPs []string `json:"Ips"`
}
func (l *Limiter) GetOnlineUserIp(tag string) ([]UserIp, error) {
if value, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := value.(*InboundInfo)
// Clear Speed Limiter bucket for users who are not online
inboundInfo.BucketHub.Range(func(key, value interface{}) bool {
if _, exists := inboundInfo.UserOnlineIP.Load(key.(string)); !exists {
inboundInfo.BucketHub.Delete(key.(string))
}
return true
})
onlineUser := make([]UserIp, 0)
var ipMap *sync.Map
inboundInfo.UserOnlineIP.Range(func(key, value interface{}) bool {
ipMap = value.(*sync.Map)
var ip []string
ipMap.Range(func(key, v interface{}) bool {
if v.(bool) {
ip = append(ip, key.(string))
}
return true
})
if len(ip) > 0 {
if u, ok := inboundInfo.UserInfo.Load(key.(string)); ok {
onlineUser = append(onlineUser, UserIp{
Uid: u.(UserInfo).UID,
IPs: ip,
})
}
}
return true
})
if len(onlineUser) == 0 {
return nil, nil
}
return onlineUser, nil
} else {
return nil, fmt.Errorf("no such inbound in limiter: %s", tag)
}
}
func (l *Limiter) UpdateOnlineUserIP(tag string, userIpList []UserIp) {
if v, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := v.(*InboundInfo)
//Clear old IP
inboundInfo.UserOnlineIP.Range(func(key, value interface{}) bool {
inboundInfo.UserOnlineIP.Delete(key)
return true
})
// Update User Online IP
for i := range userIpList {
ipMap := new(sync.Map)
for _, userIp := range (userIpList)[i].IPs {
ipMap.Store(userIp, false)
}
inboundInfo.UserOnlineIP.Store((userIpList)[i].Uid, ipMap)
}
}
}
func (l *Limiter) ClearOnlineUserIP(tag string) {
if v, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := v.(*InboundInfo)
inboundInfo.UserOnlineIP.Range(func(key, value interface{}) bool {
inboundInfo.UserOnlineIP.Delete(key)
return true
})
}
}
func (l *Limiter) GetUserBucket(tag string, email string, ip string) (limiter *ratelimit.Bucket, SpeedLimit bool, Reject bool) {
if value, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := value.(*InboundInfo)
nodeLimit := inboundInfo.NodeSpeedLimit
var userLimit uint64 = 0
var deviceLimit = 0
if v, ok := inboundInfo.UserInfo.Load(email); ok {
u := v.(UserInfo)
userLimit = u.SpeedLimit
deviceLimit = u.DeviceLimit
}
ipMap := new(sync.Map)
ipMap.Store(ip, true)
// If any device is online
if v, ok := inboundInfo.UserOnlineIP.LoadOrStore(email, ipMap); ok {
ipMap := v.(*sync.Map)
// If this ip is a new device
if online, ok := ipMap.LoadOrStore(ip, true); !ok {
counter := 0
ipMap.Range(func(key, value interface{}) bool {
counter++
return true
})
if counter > deviceLimit && deviceLimit > 0 {
ipMap.Delete(ip)
return nil, false, true
}
} else {
if !online.(bool) {
ipMap.Store(ip, true)
}
}
}
limit := determineRate(nodeLimit, userLimit) // If need the Speed limit
if limit > 0 {
limiter := ratelimit.NewBucketWithQuantum(time.Second, int64(limit), int64(limit)) // Byte/s
if v, ok := inboundInfo.BucketHub.LoadOrStore(email, limiter); ok {
bucket := v.(*ratelimit.Bucket)
return bucket, true, false
} else {
return limiter, true, false
}
} else {
return nil, false, false
}
} else {
newError("Get Inbound Limiter information failed").AtDebug().WriteToLog()
return nil, false, false
}
}
// determineRate returns the minimum non-zero rate
func determineRate(nodeLimit, userLimit uint64) (limit uint64) {
if nodeLimit == 0 || userLimit == 0 {
if nodeLimit > userLimit {
return nodeLimit
} else if nodeLimit < userLimit {
return userLimit
} else {
return 0
}
} else {
if nodeLimit > userLimit {
return userLimit
} else if nodeLimit < userLimit {
return nodeLimit
} else {
return nodeLimit
}
}
}

31
app/limiter/rate.go Normal file
View File

@@ -0,0 +1,31 @@
package limiter
import (
"io"
"github.com/juju/ratelimit"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type Writer struct {
writer buf.Writer
limiter *ratelimit.Bucket
w io.Writer
}
func (l *Limiter) RateWriter(writer buf.Writer, limiter *ratelimit.Bucket) buf.Writer {
return &Writer{
writer: writer,
limiter: limiter,
}
}
func (w *Writer) Close() error {
return common.Close(w.writer)
}
func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error {
w.limiter.Wait(int64(mb.Len()))
return w.writer.WriteMultiBuffer(mb)
}

9
app/rule/errors.go Normal file
View File

@@ -0,0 +1,9 @@
package rule
import "github.com/xtls/xray-core/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

105
app/rule/rule.go Normal file
View File

@@ -0,0 +1,105 @@
// Package rule is to control the audit rule behaviors
package rule
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"github.com/Yuzuki616/V2bX/api"
mapset "github.com/deckarep/golang-set"
)
type Rule struct {
InboundRule *sync.Map // Key: Tag, Value: []api.DetectRule
InboundProtocolRule *sync.Map // Key: Tag, Value: []string
InboundDetectResult *sync.Map // key: Tag, Value: mapset.NewSet []api.DetectResult
}
func New() *Rule {
return &Rule{
InboundRule: new(sync.Map),
InboundProtocolRule: new(sync.Map),
InboundDetectResult: new(sync.Map),
}
}
func (r *Rule) UpdateRule(tag string, newRuleList []api.DetectRule) error {
if value, ok := r.InboundRule.LoadOrStore(tag, newRuleList); ok {
oldRuleList := value.([]api.DetectRule)
if !reflect.DeepEqual(oldRuleList, newRuleList) {
r.InboundRule.Store(tag, newRuleList)
}
}
return nil
}
func (r *Rule) UpdateProtocolRule(tag string, ruleList []string) error {
if value, ok := r.InboundProtocolRule.LoadOrStore(tag, ruleList); ok {
old := value.([]string)
if !reflect.DeepEqual(old, ruleList) {
r.InboundProtocolRule.Store(tag, ruleList)
}
}
return nil
}
func (r *Rule) GetDetectResult(tag string) ([]api.DetectResult, error) {
detectResult := make([]api.DetectResult, 0)
if value, ok := r.InboundDetectResult.LoadAndDelete(tag); ok {
resultSet := value.(mapset.Set)
it := resultSet.Iterator()
for result := range it.C {
detectResult = append(detectResult, result.(api.DetectResult))
}
}
return detectResult, nil
}
func (r *Rule) Detect(tag string, destination string, email string) (reject bool) {
reject = false
var hitRuleID = -1
// If we have some rule for this inbound
if value, ok := r.InboundRule.Load(tag); ok {
ruleList := value.([]api.DetectRule)
for _, r := range ruleList {
if r.Pattern.Match([]byte(destination)) {
hitRuleID = r.ID
reject = true
break
}
}
// If we hit some rule
if reject && hitRuleID != -1 {
l := strings.Split(email, "|")
uid, err := strconv.Atoi(l[len(l)-1])
if err != nil {
newError(fmt.Sprintf("Record illegal behavior failed! Cannot find user's uid: %s", email)).AtDebug().WriteToLog()
return reject
}
newSet := mapset.NewSetWith(api.DetectResult{UID: uid, RuleID: hitRuleID})
// If there are any hit history
if v, ok := r.InboundDetectResult.LoadOrStore(tag, newSet); ok {
resultSet := v.(mapset.Set)
// If this is a new record
if resultSet.Add(api.DetectResult{UID: uid, RuleID: hitRuleID}) {
r.InboundDetectResult.Store(tag, resultSet)
}
}
}
}
return reject
}
func (r *Rule) ProtocolDetect(tag string, protocol string) bool {
if value, ok := r.InboundProtocolRule.Load(tag); ok {
ruleList := value.([]string)
for _, r := range ruleList {
if r == protocol {
return true
}
}
}
return false
}