diff --git a/common/singbridge/tls.go b/common/singbridge/tls.go index 08adfd6ec62..592345a57c5 100644 --- a/common/singbridge/tls.go +++ b/common/singbridge/tls.go @@ -3,6 +3,7 @@ package singbridge import ( "context" "crypto/tls" + "time" singtls "github.com/sagernet/sing/common/tls" @@ -50,6 +51,14 @@ func (c *tlsConfigWrapper) Client(_ net.Conn) (singtls.Conn, error) { panic("invalid") } +func (c *tlsConfigWrapper) HandshakeTimeout() time.Duration { + panic("invalid") +} + +func (c *tlsConfigWrapper) SetHandshakeTimeout(_ time.Duration) { + panic("invalid") +} + func (c *tlsConfigWrapper) Clone() singtls.Config { panic("invalid") } diff --git a/go.mod b/go.mod index 21adde4fe29..5b41c58b868 100644 --- a/go.mod +++ b/go.mod @@ -22,11 +22,12 @@ require ( github.com/pires/go-proxyproto v0.15.0 github.com/quic-go/quic-go v0.60.0 github.com/refraction-networking/utls v1.8.3-0.20260623165621-880e27d8b0e5 - github.com/sagernet/sing v0.8.11 + github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-quic v0.6.3 github.com/sagernet/sing-shadowsocks v0.2.9 github.com/sagernet/sing-shadowsocks2 v0.2.2 + github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 diff --git a/go.sum b/go.sum index 2aa89ccf343..33117591631 100644 --- a/go.sum +++ b/go.sum @@ -225,8 +225,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.11 h1:AKZRvjFPHtAXwGCjOJrzAQPiZxr8mobhuSUqkHf+VQw= -github.com/sagernet/sing v0.8.11/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.3 h1:0wSPqCNJsC7CadB5GZS1aPkyU27aRwHPd5SlsijbBNw= @@ -235,6 +235,8 @@ github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4 github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= github.com/sagernet/sing-shadowsocks2 v0.2.2 h1:GUN6ttP3NSJL/bFJJZZezp9/o6DQdG9qWyQdRvOy2LY= github.com/sagernet/sing-shadowsocks2 v0.2.2/go.mod h1:ZAYv1VFjvnMK4QQJltuvoN52biUN76TdNwzyDjSRrzc= +github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg= +github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= diff --git a/infra/conf/v4/snell.go b/infra/conf/v4/snell.go new file mode 100644 index 00000000000..03838e1c47d --- /dev/null +++ b/infra/conf/v4/snell.go @@ -0,0 +1,37 @@ +package v4 + +import ( + "github.com/golang/protobuf/proto" + + "github.com/exclavenetwork/exclave-core/v5/infra/conf/cfgcommon" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell" +) + +type SnellClientConfig struct { + Address *cfgcommon.Address `json:"address"` + Port uint16 `json:"port"` + PSK string `json:"psk"` + UserKey string `json:"userKey"` + Version uint32 `json:"version"` + Reuse bool `json:"reuse"` + ObfsMode string `json:"obfsMode"` + ObfsHost string `json:"obfsHost"` + Mode string `json:"mode"` +} + +func (c *SnellClientConfig) Build() (proto.Message, error) { + if c.Address == nil { + return nil, newError("missing server address") + } + return &snell.ClientConfig{ + Address: c.Address.Build(), + Port: uint32(c.Port), + Psk: c.PSK, + UserKey: c.UserKey, + Version: c.Version, + Reuse: c.Reuse, + ObfsMode: c.ObfsMode, + ObfsHost: c.ObfsHost, + Mode: c.Mode, + }, nil +} diff --git a/infra/conf/v4/v2ray.go b/infra/conf/v4/v2ray.go index cd347184abb..aab85b74afe 100644 --- a/infra/conf/v4/v2ray.go +++ b/infra/conf/v4/v2ray.go @@ -62,6 +62,7 @@ var ( "tuic": func() interface{} { return new(TuicClientConfig) }, "juicity": func() interface{} { return new(JuicityClientConfig) }, "mieru": func() interface{} { return new(MieruClientConfig) }, + "snell": func() interface{} { return new(SnellClientConfig) }, "trusttunnel": func() interface{} { return new(TrustTunnelClientConfig) }, }, "protocol", "settings") ) diff --git a/main/distro/all/all.go b/main/distro/all/all.go index 45bb2ff7100..b2d1220a08d 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -64,6 +64,7 @@ import ( _ "github.com/exclavenetwork/exclave-core/v5/proxy/hysteria2" _ "github.com/exclavenetwork/exclave-core/v5/proxy/juicity" _ "github.com/exclavenetwork/exclave-core/v5/proxy/mieru" + _ "github.com/exclavenetwork/exclave-core/v5/proxy/snell" _ "github.com/exclavenetwork/exclave-core/v5/proxy/tuic" _ "github.com/exclavenetwork/exclave-core/v5/proxy/wireguard" diff --git a/proxy/snell/config.pb.go b/proxy/snell/config.pb.go new file mode 100644 index 00000000000..684d16fb505 --- /dev/null +++ b/proxy/snell/config.pb.go @@ -0,0 +1,194 @@ +package snell + +import ( + net "github.com/exclavenetwork/exclave-core/v5/common/net" + _ "github.com/exclavenetwork/exclave-core/v5/common/protoext" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Psk string `protobuf:"bytes,3,opt,name=psk,proto3" json:"psk,omitempty"` + Version uint32 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"` + UserKey string `protobuf:"bytes,5,opt,name=user_key,json=userKey,proto3" json:"user_key,omitempty"` + Reuse bool `protobuf:"varint,6,opt,name=reuse,proto3" json:"reuse,omitempty"` + ObfsMode string `protobuf:"bytes,7,opt,name=obfs_mode,json=obfsMode,proto3" json:"obfs_mode,omitempty"` + ObfsHost string `protobuf:"bytes,8,opt,name=obfs_host,json=obfsHost,proto3" json:"obfs_host,omitempty"` + Mode string `protobuf:"bytes,9,opt,name=mode,proto3" json:"mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_snell_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_snell_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_snell_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ClientConfig) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ClientConfig) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ClientConfig) GetPsk() string { + if x != nil { + return x.Psk + } + return "" +} + +func (x *ClientConfig) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ClientConfig) GetUserKey() string { + if x != nil { + return x.UserKey + } + return "" +} + +func (x *ClientConfig) GetReuse() bool { + if x != nil { + return x.Reuse + } + return false +} + +func (x *ClientConfig) GetObfsMode() string { + if x != nil { + return x.ObfsMode + } + return "" +} + +func (x *ClientConfig) GetObfsHost() string { + if x != nil { + return x.ObfsHost + } + return "" +} + +func (x *ClientConfig) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +var File_proxy_snell_config_proto protoreflect.FileDescriptor + +const file_proxy_snell_config_proto_rawDesc = "" + + "\n" + + "\x18proxy/snell/config.proto\x12\x18exclave.core.proxy.snell\x1a common/protoext/extensions.proto\x1a\x18common/net/address.proto\"\xa3\x02\n" + + "\fClientConfig\x12=\n" + + "\aaddress\x18\x01 \x01(\v2#.exclave.core.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12\x10\n" + + "\x03psk\x18\x03 \x01(\tR\x03psk\x12\x18\n" + + "\aversion\x18\x04 \x01(\rR\aversion\x12\x19\n" + + "\buser_key\x18\x05 \x01(\tR\auserKey\x12\x14\n" + + "\x05reuse\x18\x06 \x01(\bR\x05reuse\x12\x1b\n" + + "\tobfs_mode\x18\a \x01(\tR\bobfsMode\x12\x1b\n" + + "\tobfs_host\x18\b \x01(\tR\bobfsHost\x12\x12\n" + + "\x04mode\x18\t \x01(\tR\x04mode:\x15\x82\xb5\x18\x11\n" + + "\boutbound\x12\x05snellB\x88\x01\n" + + "2com.github.exclavenetwork.exclave.core.proxy.snellP\x01Z5github.com/exclavenetwork/exclave-core/v5/proxy/snell\xaa\x02\x18Exclave.Core.Proxy.Snellb\x06proto3" + +var ( + file_proxy_snell_config_proto_rawDescOnce sync.Once + file_proxy_snell_config_proto_rawDescData []byte +) + +func file_proxy_snell_config_proto_rawDescGZIP() []byte { + file_proxy_snell_config_proto_rawDescOnce.Do(func() { + file_proxy_snell_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_snell_config_proto_rawDesc), len(file_proxy_snell_config_proto_rawDesc))) + }) + return file_proxy_snell_config_proto_rawDescData +} + +var file_proxy_snell_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_snell_config_proto_goTypes = []any{ + (*ClientConfig)(nil), // 0: exclave.core.proxy.snell.ClientConfig + (*net.IPOrDomain)(nil), // 1: exclave.core.common.net.IPOrDomain +} +var file_proxy_snell_config_proto_depIdxs = []int32{ + 1, // 0: exclave.core.proxy.snell.ClientConfig.address:type_name -> exclave.core.common.net.IPOrDomain + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_snell_config_proto_init() } +func file_proxy_snell_config_proto_init() { + if File_proxy_snell_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_snell_config_proto_rawDesc), len(file_proxy_snell_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_snell_config_proto_goTypes, + DependencyIndexes: file_proxy_snell_config_proto_depIdxs, + MessageInfos: file_proxy_snell_config_proto_msgTypes, + }.Build() + File_proxy_snell_config_proto = out.File + file_proxy_snell_config_proto_goTypes = nil + file_proxy_snell_config_proto_depIdxs = nil +} diff --git a/proxy/snell/config.proto b/proxy/snell/config.proto new file mode 100644 index 00000000000..784ec58b138 --- /dev/null +++ b/proxy/snell/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package exclave.core.proxy.snell; +option csharp_namespace = "Exclave.Core.Proxy.Snell"; +option go_package = "github.com/exclavenetwork/exclave-core/v5/proxy/snell"; +option java_package = "com.github.exclavenetwork.exclave.core.proxy.snell"; +option java_multiple_files = true; + +import "common/protoext/extensions.proto"; +import "common/net/address.proto"; + +message ClientConfig { + option (exclave.core.common.protoext.message_opt).type = "outbound"; + option (exclave.core.common.protoext.message_opt).short_name = "snell"; + + exclave.core.common.net.IPOrDomain address = 1; + uint32 port = 2; + string psk = 3; + uint32 version = 4; + string user_key = 5; + bool reuse = 6; + string obfs_mode = 7; + string obfs_host = 8; + string mode = 9; +} diff --git a/proxy/snell/errors.generated.go b/proxy/snell/errors.generated.go new file mode 100644 index 00000000000..8ce20d7b51e --- /dev/null +++ b/proxy/snell/errors.generated.go @@ -0,0 +1,9 @@ +package snell + +import "github.com/exclavenetwork/exclave-core/v5/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go new file mode 100644 index 00000000000..6b32f18a1d5 --- /dev/null +++ b/proxy/snell/outbound.go @@ -0,0 +1,215 @@ +package snell + +import ( + "context" + "net" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/snellv4" + "github.com/sagernet/sing-snell/snellv6" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/network" + + core "github.com/exclavenetwork/exclave-core/v5" + "github.com/exclavenetwork/exclave-core/v5/app/proxyman/outbound" + "github.com/exclavenetwork/exclave-core/v5/common" + v2net "github.com/exclavenetwork/exclave-core/v5/common/net" + "github.com/exclavenetwork/exclave-core/v5/common/session" + "github.com/exclavenetwork/exclave-core/v5/common/singbridge" + "github.com/exclavenetwork/exclave-core/v5/transport" + "github.com/exclavenetwork/exclave-core/v5/transport/internet" +) + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} + +var ( + _ snellClient = (*snellv4.Client)(nil) + _ snellClient = (*snellv6.Client)(nil) +) + +type snellClient interface { + DialContext(ctx context.Context, destination metadata.Socksaddr) (net.Conn, error) + DialPacketConn(conn net.Conn) (network.NetPacketConn, error) + Close() error +} + +type Outbound struct { + serverAddr v2net.Destination + psk []byte + userKey []byte + obfsMode snell.ObfsMode + obfsHost string + version uint32 + reuse bool + mode snellv6.Mode + client snellClient + clientAccess sync.Mutex +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { + if config.Address == nil { + return nil, newError("missing server address") + } + outbound := &Outbound{ + serverAddr: v2net.Destination{ + Address: config.Address.AsAddress(), + Port: v2net.Port(config.Port), + Network: v2net.Network_TCP, + }, + psk: []byte(config.Psk), + userKey: []byte(config.UserKey), + version: config.Version, + reuse: config.Reuse, + } + + if outbound.version != 4 && outbound.version != 6 { + return nil, newError("unsupported snell version: ", outbound.version) + } + + if outbound.version == 4 { + switch config.ObfsMode { + case "", "none": + outbound.obfsMode = snell.ObfsModeNone + if config.ObfsHost != "" { + return nil, newError(`invalid obfsHost for obfsMode "none"`) + } + case "http": + outbound.obfsMode = snell.ObfsModeHTTP + outbound.obfsHost = config.ObfsHost + case "tls": + outbound.obfsMode = snell.ObfsModeTLS + outbound.obfsHost = config.ObfsHost + default: + return nil, newError("invalid snell obfsMode: ", config.ObfsMode) + } + if config.Mode != "" { + return nil, newError("mode is only valid for version 6") + } + } + + if outbound.version == 6 { + switch config.Mode { + case "", "default": + outbound.mode = snellv6.ModeDefault + case "unshaped": + outbound.mode = snellv6.ModeUnshaped + case "unsafe-raw": + outbound.mode = snellv6.ModeUnsafeRaw + default: + return nil, newError("invalid snell mode: ", config.Mode) + } + if config.ObfsHost != "" { + return nil, newError("obfsHost is only valid for version 4") + } + if config.ObfsMode != "" { + return nil, newError("obfsMode is only valid for version 4") + } + } + + return outbound, nil +} + +func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { + o.clientAccess.Lock() + defer o.clientAccess.Unlock() + if o.client != nil { + return o.client, nil + } + handler, ok := dialer.(*outbound.Handler) + if !ok { + panic("dialer is not *outbound.Handler") + } + if handler.MuxEnabled() { + return nil, newError("mux enabled") + } + if handler.TransportLayerEnabled() { + return nil, newError("transport layer enabled") + } + if streamSettings := handler.StreamSettings(); streamSettings != nil && streamSettings.SecurityType != "" { + return nil, newError("tls enabled") + } + + var client snellClient + var err error + if o.version == 6 { + client, err = snellv6.NewClient(snellv6.ClientOptions{ + PSK: o.psk, + UserKey: o.userKey, + Mode: o.mode, + Reuse: o.reuse, + Dialer: singbridge.NewDialerWrapper(dialer), + Server: metadata.ParseSocksaddr(o.serverAddr.NetAddr()), + }) + } else { + client, err = snellv4.NewClient(snellv4.ClientOptions{ + PSK: o.psk, + UserKey: o.userKey, + Reuse: o.reuse, + ObfsMode: o.obfsMode, + ObfsHost: o.obfsHost, + Dialer: singbridge.NewDialerWrapper(dialer), + Server: metadata.ParseSocksaddr(o.serverAddr.NetAddr()), + }) + } + if err != nil { + return nil, err + } + o.client = client + return client, nil +} + +func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + client, err := o.getClient(dialer) + if err != nil { + return err + } + + ob := session.OutboundFromContext(ctx) + if ob == nil || !ob.Target.IsValid() { + return newError("target not specified") + } + destination := ob.Target + + newError("tunneling request to ", destination, " via ", o.serverAddr.NetAddr()).WriteToLog(session.ExportIDToError(ctx)) + + detachedCtx := core.ToBackgroundDetachedContext(ctx) + + if destination.Network == v2net.Network_TCP { + serverConn, err := client.DialContext(detachedCtx, singbridge.ToSocksAddr(destination)) + if err != nil { + return err + } + return singbridge.ReturnError(bufio.CopyConn(detachedCtx, singbridge.NewPipeConnWrapper(link), serverConn)) + } else { + rawConn, err := dialer.Dial(detachedCtx, o.serverAddr) + if err != nil { + return err + } + serverConn, err := client.DialPacketConn(rawConn) + if err != nil { + rawConn.Close() + return err + } + return singbridge.ReturnError(bufio.CopyPacketConn(detachedCtx, singbridge.NewPacketConnWrapper(link, destination), serverConn)) + } +} + +func (o *Outbound) InterfaceUpdate() { + _ = o.Close() +} + +func (o *Outbound) Close() error { + o.clientAccess.Lock() + if o.client != nil { + _ = o.client.Close() + o.client = nil + } + o.clientAccess.Unlock() + return nil +} diff --git a/proxy/snell/snell.go b/proxy/snell/snell.go new file mode 100644 index 00000000000..dc671207f68 --- /dev/null +++ b/proxy/snell/snell.go @@ -0,0 +1,3 @@ +package snell + +//go:generate go run github.com/exclavenetwork/exclave-core/v5/common/errors/errorgen