mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-13 11:05:46 +00:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd3ced5894 | ||
|
|
e77250a40e | ||
|
|
a659344f67 | ||
|
|
36b75894c0 | ||
|
|
dc70f480ae | ||
|
|
a2f6a4b154 | ||
|
|
622207bb96 | ||
|
|
6bc4d4a3e8 | ||
|
|
e33aad40d0 | ||
|
|
105d4547f4 | ||
|
|
1b28c19333 | ||
|
|
d39965f20d | ||
|
|
81bcfcae77 | ||
|
|
e645ed69aa |
@@ -37,7 +37,6 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
|
||||
|
||||
# Create log files
|
||||
|
||||
@@ -37,7 +37,6 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
|
||||
|
||||
# Create log files
|
||||
|
||||
@@ -64,14 +64,6 @@ jobs:
|
||||
echo "Latest: '$LATEST'."
|
||||
echo "LATEST=$LATEST" >>${GITHUB_ENV}
|
||||
|
||||
NEWEST=false
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
NEWEST=true
|
||||
fi
|
||||
|
||||
echo "Newest: '$NEWEST'."
|
||||
echo "NEWEST=$NEWEST" >>${GITHUB_ENV}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
@@ -132,13 +124,6 @@ jobs:
|
||||
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
fi
|
||||
|
||||
if [[ "${{ env.NEWEST }}" == "true" ]]; then
|
||||
echo "Adding 'pre-release' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:pre-release'."
|
||||
docker buildx imagetools create \
|
||||
--tag ${{ env.FULL_IMAGE_NAME }}:pre-release \
|
||||
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
fi
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
@@ -146,7 +131,3 @@ jobs:
|
||||
if [[ "${{ env.LATEST }}" == "true" ]]; then
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
|
||||
fi
|
||||
|
||||
if [[ "${{ env.NEWEST }}" == "true" ]]; then
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
|
||||
fi
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v7
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v7
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -162,7 +162,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
|
||||
p := d.policy.ForLevel(user.Level)
|
||||
if p.Stats.UserUplink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
|
||||
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
|
||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
||||
inboundLink.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: inboundLink.Writer,
|
||||
@@ -171,7 +171,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
|
||||
}
|
||||
if p.Stats.UserDownlink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
|
||||
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
|
||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
||||
outboundLink.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: outboundLink.Writer,
|
||||
@@ -200,13 +200,13 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
|
||||
p := policyManager.ForLevel(user.Level)
|
||||
if p.Stats.UserUplink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
|
||||
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
|
||||
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
|
||||
link.Reader.(*buf.TimeoutWrapperReader).Counter = c
|
||||
}
|
||||
}
|
||||
if p.Stats.UserDownlink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
|
||||
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
|
||||
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
|
||||
link.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: link.Writer,
|
||||
@@ -223,7 +223,7 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
|
||||
|
||||
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
|
||||
name := "user>>>" + email + ">>>online"
|
||||
if om, _ := sm.GetOrRegisterOnlineMap(name); om != nil {
|
||||
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
|
||||
om.AddIP(ip)
|
||||
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
|
||||
}
|
||||
|
||||
+11
-22
@@ -330,7 +330,6 @@ type SenderConfig struct {
|
||||
// Send traffic through the given IP. Only IP is allowed.
|
||||
Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
|
||||
StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
|
||||
ProxySettings *internet.ProxyConfig `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"`
|
||||
MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
|
||||
ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
|
||||
TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"`
|
||||
@@ -382,13 +381,6 @@ func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig {
|
||||
if x != nil {
|
||||
return x.ProxySettings
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig {
|
||||
if x != nil {
|
||||
return x.MultiplexSettings
|
||||
@@ -506,14 +498,13 @@ const file_app_proxyman_config_proto_rawDesc = "" +
|
||||
"\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" +
|
||||
"\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" +
|
||||
"\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" +
|
||||
"\x0eOutboundConfig\"\x9d\x03\n" +
|
||||
"\x0eOutboundConfig\"\xd6\x02\n" +
|
||||
"\fSenderConfig\x12-\n" +
|
||||
"\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" +
|
||||
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12K\n" +
|
||||
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
|
||||
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12T\n" +
|
||||
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
|
||||
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
|
||||
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" +
|
||||
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategyJ\x04\b\x03\x10\x04\"\xa4\x01\n" +
|
||||
"\x12MultiplexingConfig\x12\x18\n" +
|
||||
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
|
||||
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
|
||||
@@ -548,8 +539,7 @@ var file_app_proxyman_config_proto_goTypes = []any{
|
||||
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
|
||||
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
|
||||
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
|
||||
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig
|
||||
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
|
||||
(internet.DomainStrategy)(0), // 13: xray.transport.internet.DomainStrategy
|
||||
}
|
||||
var file_app_proxyman_config_proto_depIdxs = []int32{
|
||||
7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule
|
||||
@@ -562,14 +552,13 @@ var file_app_proxyman_config_proto_depIdxs = []int32{
|
||||
12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
|
||||
10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
|
||||
11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
|
||||
13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig
|
||||
6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
|
||||
14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
6, // 10: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
|
||||
13, // 11: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||
12, // [12:12] is the sub-list for method output_type
|
||||
12, // [12:12] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_app_proxyman_config_proto_init() }
|
||||
|
||||
@@ -57,7 +57,7 @@ message SenderConfig {
|
||||
// Send traffic through the given IP. Only IP is allowed.
|
||||
xray.common.net.IPOrDomain via = 1;
|
||||
xray.transport.internet.StreamConfig stream_settings = 2;
|
||||
xray.transport.internet.ProxyConfig proxy_settings = 3;
|
||||
reserved 3;
|
||||
MultiplexingConfig multiplex_settings = 4;
|
||||
string via_cidr = 5;
|
||||
xray.transport.internet.DomainStrategy target_strategy = 6;
|
||||
|
||||
@@ -26,7 +26,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/mux"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/net/cnc"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/core"
|
||||
@@ -26,8 +25,6 @@ import (
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"github.com/xtls/xray-core/transport/pipe"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -39,7 +36,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -47,7 +44,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
@@ -63,7 +60,6 @@ type Handler struct {
|
||||
streamSettings *internet.MemoryStreamConfig
|
||||
proxyConfig proto.Message
|
||||
proxy proxy.Outbound
|
||||
outboundManager outbound.Manager
|
||||
mux *mux.ClientManager
|
||||
xudp *mux.ClientManager
|
||||
udp443 string
|
||||
@@ -77,7 +73,6 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
||||
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
|
||||
h := &Handler{
|
||||
tag: config.Tag,
|
||||
outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager),
|
||||
uplinkCounter: uplinkCounter,
|
||||
downlinkCounter: downlinkCounter,
|
||||
}
|
||||
@@ -108,7 +103,10 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
||||
|
||||
ctx = session.ContextWithFullHandler(ctx, h)
|
||||
|
||||
newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings)
|
||||
newCtx := ctx
|
||||
if h.streamSettings != nil {
|
||||
newCtx = session.ContextWithStreamSettings(newCtx, h.streamSettings)
|
||||
}
|
||||
|
||||
rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig)
|
||||
if err != nil {
|
||||
@@ -270,43 +268,10 @@ func (h *Handler) DestIpAddress() net.IP {
|
||||
|
||||
// Dial implements internet.Dialer.
|
||||
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
|
||||
if h.senderSettings != nil {
|
||||
|
||||
if h.senderSettings.ProxySettings.HasTag() {
|
||||
|
||||
tag := h.senderSettings.ProxySettings.Tag
|
||||
handler := h.outboundManager.GetHandler(tag)
|
||||
if handler != nil {
|
||||
errors.LogDebug(ctx, "proxying to ", tag, " for dest ", dest)
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ctx = session.ContextWithOutbounds(ctx, append(outbounds, &session.Outbound{
|
||||
Target: dest,
|
||||
Tag: tag,
|
||||
})) // add another outbound in session ctx
|
||||
opts := pipe.OptionsFromContext(ctx)
|
||||
uplinkReader, uplinkWriter := pipe.New(opts...)
|
||||
downlinkReader, downlinkWriter := pipe.New(opts...)
|
||||
|
||||
go handler.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter})
|
||||
conn := cnc.NewConnection(cnc.ConnectionInputMulti(uplinkWriter), cnc.ConnectionOutputMulti(downlinkReader))
|
||||
|
||||
if config := tls.ConfigFromStreamSettings(h.streamSettings); config != nil {
|
||||
tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
|
||||
conn = tls.Client(conn, tlsConfig)
|
||||
}
|
||||
|
||||
return h.getStatCouterConnection(conn), nil
|
||||
}
|
||||
|
||||
errors.LogError(ctx, "failed to get outbound handler with tag: ", tag)
|
||||
return nil, errors.New("failed to get outbound handler with tag: " + tag)
|
||||
}
|
||||
|
||||
if h.senderSettings.Via != nil {
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
h.SetOutboundGateway(ctx, ob)
|
||||
}
|
||||
if h.senderSettings != nil && h.senderSettings.Via != nil {
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
h.SetOutboundGateway(ctx, ob)
|
||||
}
|
||||
|
||||
conn, err := internet.Dial(ctx, dest, h.streamSettings)
|
||||
@@ -322,14 +287,14 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti
|
||||
}
|
||||
|
||||
func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) {
|
||||
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil && !h.senderSettings.ProxySettings.HasTag() && (h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
|
||||
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil &&
|
||||
(h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
|
||||
var domain string
|
||||
addr := h.senderSettings.Via.AsAddress()
|
||||
domain = h.senderSettings.Via.GetDomain()
|
||||
switch {
|
||||
case h.senderSettings.ViaCidr != "":
|
||||
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
|
||||
|
||||
case domain == "origin":
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil {
|
||||
if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() {
|
||||
@@ -344,12 +309,9 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
|
||||
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
|
||||
}
|
||||
}
|
||||
// case addr.Family().IsDomain():
|
||||
default:
|
||||
default: // case addr.Family().IsDomain():
|
||||
ob.Gateway = addr
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,20 +48,6 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if c, found := m.counters[name]; found {
|
||||
return c, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new counter ", name)
|
||||
c := new(Counter)
|
||||
m.counters[name] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// UnregisterCounter implements stats.Manager.
|
||||
func (m *Manager) UnregisterCounter(name string) error {
|
||||
m.access.Lock()
|
||||
@@ -111,20 +97,6 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
|
||||
return om, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterOnlineMap(name string) (stats.OnlineMap, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if om, found := m.onlineMaps[name]; found {
|
||||
return om, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new OnlineMap ", name)
|
||||
om := NewOnlineMap()
|
||||
m.onlineMaps[name] = om
|
||||
return om, nil
|
||||
}
|
||||
|
||||
// UnregisterOnlineMap implements stats.Manager.
|
||||
func (m *Manager) UnregisterOnlineMap(name string) error {
|
||||
m.access.Lock()
|
||||
@@ -177,26 +149,6 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterChannel(name string) (stats.Channel, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if c, found := m.channels[name]; found {
|
||||
return c, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new channel ", name)
|
||||
c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false})
|
||||
if m.running {
|
||||
// Start before publishing so no goroutine can observe an unstarted channel.
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m.channels[name] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// UnregisterChannel implements stats.Manager.
|
||||
func (m *Manager) UnregisterChannel(name string) error {
|
||||
m.access.Lock()
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
|
||||
}
|
||||
|
||||
_, isFile := reader.(*os.File)
|
||||
if !isFile && useReadV() {
|
||||
if !isFile && useReadv {
|
||||
if sc, ok := reader.(syscall.Conn); ok {
|
||||
rawConn, err := sc.SyscallConn()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,6 @@ package buf
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
@@ -144,24 +143,13 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
|
||||
return mb, nil
|
||||
}
|
||||
|
||||
var useReadv atomic.Bool
|
||||
|
||||
func useReadV() bool {
|
||||
return useReadv.Load()
|
||||
}
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
|
||||
enabled := false
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
enabled = true
|
||||
}
|
||||
useReadv.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
var useReadv bool
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
useReadv = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ import (
|
||||
"github.com/xtls/xray-core/features/stats"
|
||||
)
|
||||
|
||||
func useReadV() bool {
|
||||
return false
|
||||
}
|
||||
const useReadv = false
|
||||
|
||||
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
|
||||
panic("not implemented")
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinProcPIDListFDs = 1
|
||||
darwinProcPIDFDSocketInfo = 3
|
||||
darwinProcFDTypeSocket = 2
|
||||
darwinProcFDInfoSize = 8
|
||||
darwinSocketFDInfoSize = 792
|
||||
darwinSocketFDInfoPSIOff = 24
|
||||
darwinSocketInfoProtoOff = darwinSocketFDInfoPSIOff + 156
|
||||
darwinSocketInfoFamilyOff = darwinSocketFDInfoPSIOff + 160
|
||||
darwinSocketInfoKindOff = darwinSocketFDInfoPSIOff + 232
|
||||
darwinSocketInfoInSockOff = darwinSocketFDInfoPSIOff + 240
|
||||
darwinInSockInfoFPortOff = darwinSocketInfoInSockOff
|
||||
darwinInSockInfoLPortOff = darwinSocketInfoInSockOff + 4
|
||||
darwinInSockInfoVFlagOff = darwinSocketInfoInSockOff + 24
|
||||
darwinInSockInfoFAddrOff = darwinSocketInfoInSockOff + 32
|
||||
darwinInSockInfoLAddrOff = darwinSocketInfoInSockOff + 48
|
||||
darwinInSockInfoSize = 80
|
||||
darwinInSockInfoIPv4 = 0x1
|
||||
darwinInSockInfoIPv6 = 0x2
|
||||
darwinSockInfoIN = 1
|
||||
darwinSockInfoTCP = 2
|
||||
)
|
||||
|
||||
type darwinSocketMatchLevel int
|
||||
|
||||
const (
|
||||
darwinSocketNoMatch darwinSocketMatchLevel = iota
|
||||
darwinSocketPortMatch
|
||||
darwinSocketRemoteMatch
|
||||
darwinSocketLocalMatch
|
||||
darwinSocketExactMatch
|
||||
)
|
||||
|
||||
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
|
||||
isLocal, err := IsLocal(net.ParseIP(srcIP))
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("failed to determine if address is local: ", err)
|
||||
}
|
||||
if !isLocal {
|
||||
return 0, "", "", ErrNotLocal
|
||||
}
|
||||
if network != "tcp" && network != "udp" {
|
||||
panic("Unsupported network type for process lookup.")
|
||||
}
|
||||
|
||||
srcAddr, err := netip.ParseAddr(srcIP)
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("invalid source IP address: ", srcIP)
|
||||
}
|
||||
srcAddr = srcAddr.Unmap()
|
||||
|
||||
var dstAddr netip.Addr
|
||||
hasDstAddr := false
|
||||
if destIP != "" && destPort != 0 {
|
||||
dstAddr, err = netip.ParseAddr(destIP)
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("invalid destination IP address: ", destIP)
|
||||
}
|
||||
dstAddr = dstAddr.Unmap()
|
||||
hasDstAddr = true
|
||||
}
|
||||
|
||||
processes, err := unix.SysctlKinfoProcSlice("kern.proc.all")
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("failed to list processes").Base(err)
|
||||
}
|
||||
|
||||
var bestPID int32
|
||||
bestLevel := darwinSocketNoMatch
|
||||
ambiguousBest := false
|
||||
|
||||
for _, process := range processes {
|
||||
pid := process.Proc.P_pid
|
||||
if pid <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
matchLevel, err := darwinProcessSocketMatchLevel(pid, network, srcAddr, srcPort, dstAddr, destPort, hasDstAddr)
|
||||
if err != nil || matchLevel == darwinSocketNoMatch {
|
||||
continue
|
||||
}
|
||||
if matchLevel == darwinSocketExactMatch {
|
||||
bestPID = pid
|
||||
bestLevel = matchLevel
|
||||
ambiguousBest = false
|
||||
break
|
||||
}
|
||||
if matchLevel > bestLevel {
|
||||
bestPID = pid
|
||||
bestLevel = matchLevel
|
||||
ambiguousBest = false
|
||||
continue
|
||||
}
|
||||
if matchLevel == bestLevel {
|
||||
ambiguousBest = true
|
||||
}
|
||||
}
|
||||
|
||||
if bestLevel == darwinSocketNoMatch {
|
||||
return 0, "", "", errors.New("process not found for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
|
||||
}
|
||||
if ambiguousBest {
|
||||
return 0, "", "", errors.New("ambiguous process match for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
|
||||
}
|
||||
|
||||
absPath, err := darwinProcessPath(bestPID)
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("could not get process path for PID ", bestPID, ": ", err)
|
||||
}
|
||||
|
||||
absPath = filepath.ToSlash(absPath)
|
||||
return int(bestPID), filepath.Base(absPath), absPath, nil
|
||||
}
|
||||
|
||||
func darwinProcessSocketMatchLevel(pid int32, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) (darwinSocketMatchLevel, error) {
|
||||
fds, err := darwinProcessFDs(pid)
|
||||
if err != nil {
|
||||
return darwinSocketNoMatch, err
|
||||
}
|
||||
|
||||
bestLevel := darwinSocketNoMatch
|
||||
info := make([]byte, darwinSocketFDInfoSize)
|
||||
for fd := 0; fd+darwinProcFDInfoSize <= len(fds); fd += darwinProcFDInfoSize {
|
||||
fdNumber := int32(darwinReadNativeUint32(fds[fd : fd+4]))
|
||||
fdType := darwinReadNativeUint32(fds[fd+4 : fd+8])
|
||||
if fdType != darwinProcFDTypeSocket {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := darwinProcPIDFDInfo(pid, fdNumber, darwinProcPIDFDSocketInfo, info)
|
||||
if err != nil || n < darwinSocketInfoInSockOff+darwinInSockInfoSize {
|
||||
continue
|
||||
}
|
||||
level := darwinSocketInfoMatchLevel(info[:n], network, srcAddr, srcPort, dstAddr, dstPort, hasDstAddr)
|
||||
if level == darwinSocketExactMatch {
|
||||
return level, nil
|
||||
}
|
||||
if level > bestLevel {
|
||||
bestLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
return bestLevel, nil
|
||||
}
|
||||
|
||||
func darwinProcessFDs(pid int32) ([]byte, error) {
|
||||
n, err := darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := make([]byte, n)
|
||||
n, err = darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) darwinSocketMatchLevel {
|
||||
protocol := int(darwinReadNativeUint32(info[darwinSocketInfoProtoOff : darwinSocketInfoProtoOff+4]))
|
||||
family := int(darwinReadNativeUint32(info[darwinSocketInfoFamilyOff : darwinSocketInfoFamilyOff+4]))
|
||||
kind := int(darwinReadNativeUint32(info[darwinSocketInfoKindOff : darwinSocketInfoKindOff+4]))
|
||||
|
||||
switch network {
|
||||
case "tcp":
|
||||
if protocol != unix.IPPROTO_TCP || kind != darwinSockInfoTCP {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
case "udp":
|
||||
if protocol != unix.IPPROTO_UDP || kind != darwinSockInfoIN {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
default:
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
|
||||
vflag := info[darwinInSockInfoVFlagOff]
|
||||
if srcAddr.Is4() {
|
||||
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
} else {
|
||||
if family != unix.AF_INET6 || vflag&darwinInSockInfoIPv6 == 0 {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
}
|
||||
|
||||
localPort := int32(darwinReadNativeUint32(info[darwinInSockInfoLPortOff : darwinInSockInfoLPortOff+4]))
|
||||
if !darwinPortMatches(localPort, srcPort) {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
localAddrMatches := darwinAddrMatchesOrUnspecified(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], srcAddr)
|
||||
|
||||
foreignAddrRaw := info[darwinInSockInfoFAddrOff : darwinInSockInfoFAddrOff+16]
|
||||
foreignPort := int32(darwinReadNativeUint32(info[darwinInSockInfoFPortOff : darwinInSockInfoFPortOff+4]))
|
||||
|
||||
if !hasDstAddr {
|
||||
if localAddrMatches {
|
||||
return darwinSocketExactMatch
|
||||
}
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
|
||||
remoteMatches := darwinPortMatches(foreignPort, dstPort) && darwinAddrMatches(foreignAddrRaw, dstAddr)
|
||||
if network == "udp" && darwinEndpointIsZero(foreignAddrRaw, foreignPort) && localAddrMatches {
|
||||
return darwinSocketExactMatch
|
||||
}
|
||||
switch {
|
||||
case localAddrMatches && remoteMatches:
|
||||
return darwinSocketExactMatch
|
||||
case localAddrMatches:
|
||||
return darwinSocketLocalMatch
|
||||
case remoteMatches:
|
||||
return darwinSocketRemoteMatch
|
||||
default:
|
||||
return darwinSocketPortMatch
|
||||
}
|
||||
}
|
||||
|
||||
func darwinPortMatches(value int32, port uint16) bool {
|
||||
raw := uint16(value)
|
||||
return raw == port || darwinNtohs(raw) == port
|
||||
}
|
||||
|
||||
func darwinNtohs(value uint16) uint16 {
|
||||
return value<<8 | value>>8
|
||||
}
|
||||
|
||||
func darwinAddrMatches(raw []byte, addr netip.Addr) bool {
|
||||
if addr.Is4() {
|
||||
ip := addr.As4()
|
||||
return bytes.Equal(raw[12:16], ip[:])
|
||||
}
|
||||
ip := addr.As16()
|
||||
return bytes.Equal(raw, ip[:])
|
||||
}
|
||||
|
||||
func darwinAddrMatchesOrUnspecified(raw []byte, addr netip.Addr) bool {
|
||||
if darwinAddrMatches(raw, addr) {
|
||||
return true
|
||||
}
|
||||
if addr.Is4() {
|
||||
return darwinBytesAreZero(raw[12:16])
|
||||
}
|
||||
return darwinBytesAreZero(raw)
|
||||
}
|
||||
|
||||
func darwinEndpointIsZero(rawAddr []byte, port int32) bool {
|
||||
return uint32(port) == 0 && darwinBytesAreZero(rawAddr)
|
||||
}
|
||||
|
||||
func darwinBytesAreZero(raw []byte) bool {
|
||||
for _, value := range raw {
|
||||
if value != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func darwinReadNativeUint32(b []byte) uint32 {
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
|
||||
func darwinProcessPath(pid int32) (string, error) {
|
||||
buf := make([]byte, unix.PathMax)
|
||||
n, err := darwinProcPIDPath(pid, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n <= 0 {
|
||||
return "", errors.New("empty process path")
|
||||
}
|
||||
return strings.TrimRight(string(buf[:n]), "\x00"), nil
|
||||
}
|
||||
|
||||
func darwinProcPIDInfo(pid int32, flavor int, arg uint64, buf []byte) (int, error) {
|
||||
var ptr unsafe.Pointer
|
||||
if len(buf) > 0 {
|
||||
ptr = unsafe.Pointer(&buf[0])
|
||||
}
|
||||
|
||||
r0, _, errno := syscall_syscall6(libc_proc_pidinfo_trampoline_addr, uintptr(pid), uintptr(flavor), uintptr(arg), uintptr(ptr), uintptr(len(buf)), 0)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return int(r0), nil
|
||||
}
|
||||
|
||||
func darwinProcPIDFDInfo(pid int32, fd int32, flavor int, buf []byte) (int, error) {
|
||||
var ptr unsafe.Pointer
|
||||
if len(buf) > 0 {
|
||||
ptr = unsafe.Pointer(&buf[0])
|
||||
}
|
||||
|
||||
r0, _, errno := syscall_syscall6(libc_proc_pidfdinfo_trampoline_addr, uintptr(pid), uintptr(fd), uintptr(flavor), uintptr(ptr), uintptr(len(buf)), 0)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return int(r0), nil
|
||||
}
|
||||
|
||||
func darwinProcPIDPath(pid int32, buf []byte) (int, error) {
|
||||
r0, _, errno := syscall_syscall6(libc_proc_pidpath_trampoline_addr, uintptr(pid), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0, 0)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return int(r0), nil
|
||||
}
|
||||
|
||||
var libc_proc_pidinfo_trampoline_addr uintptr
|
||||
|
||||
//go:cgo_import_dynamic libc_proc_pidinfo proc_pidinfo "/usr/lib/libproc.dylib"
|
||||
|
||||
var libc_proc_pidfdinfo_trampoline_addr uintptr
|
||||
|
||||
//go:cgo_import_dynamic libc_proc_pidfdinfo proc_pidfdinfo "/usr/lib/libproc.dylib"
|
||||
|
||||
var libc_proc_pidpath_trampoline_addr uintptr
|
||||
|
||||
//go:cgo_import_dynamic libc_proc_pidpath proc_pidpath "/usr/lib/libproc.dylib"
|
||||
|
||||
// Implemented in the runtime package (runtime/sys_darwin.go).
|
||||
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||
|
||||
//go:linkname syscall_syscall6 syscall.syscall6
|
||||
@@ -1,18 +0,0 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT libc_proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0
|
||||
JMP libc_proc_pidinfo(SB)
|
||||
GLOBL ·libc_proc_pidinfo_trampoline_addr(SB), RODATA, $8
|
||||
DATA ·libc_proc_pidinfo_trampoline_addr(SB)/8, $libc_proc_pidinfo_trampoline<>(SB)
|
||||
|
||||
TEXT libc_proc_pidfdinfo_trampoline<>(SB),NOSPLIT,$0-0
|
||||
JMP libc_proc_pidfdinfo(SB)
|
||||
GLOBL ·libc_proc_pidfdinfo_trampoline_addr(SB), RODATA, $8
|
||||
DATA ·libc_proc_pidfdinfo_trampoline_addr(SB)/8, $libc_proc_pidfdinfo_trampoline<>(SB)
|
||||
|
||||
TEXT libc_proc_pidpath_trampoline<>(SB),NOSPLIT,$0-0
|
||||
JMP libc_proc_pidpath(SB)
|
||||
GLOBL ·libc_proc_pidpath_trampoline_addr(SB), RODATA, $8
|
||||
DATA ·libc_proc_pidpath_trampoline_addr(SB)/8, $libc_proc_pidpath_trampoline<>(SB)
|
||||
@@ -1,293 +0,0 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
stdnet "net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestFindProcessDarwinTCP(t *testing.T) {
|
||||
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan stdnet.Conn, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
return
|
||||
}
|
||||
close(accepted)
|
||||
}()
|
||||
|
||||
conn, err := stdnet.Dial("tcp", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverConn := <-accepted
|
||||
if serverConn == nil {
|
||||
t.Fatal("server did not accept tcp connection")
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.TCPAddr)
|
||||
remote := conn.RemoteAddr().(*stdnet.TCPAddr)
|
||||
|
||||
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), remote.IP.String(), uint16(remote.Port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinUDP(t *testing.T) {
|
||||
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.UDPAddr)
|
||||
|
||||
pid, name, path, err := FindProcess("udp", local.IP.String(), uint16(local.Port), "", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinNonLocal(t *testing.T) {
|
||||
_, _, _, err := FindProcess("tcp", "203.0.113.1", 80, "", 0)
|
||||
if err != ErrNotLocal {
|
||||
t.Fatalf("expected ErrNotLocal, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinUnsupportedNetwork(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("expected panic")
|
||||
}
|
||||
}()
|
||||
|
||||
_, _, _, _ = FindProcess("icmp", "127.0.0.1", 0, "", 0)
|
||||
}
|
||||
|
||||
func assertCurrentProcess(t *testing.T, pid int, name string, path string) {
|
||||
t.Helper()
|
||||
|
||||
if pid != os.Getpid() {
|
||||
t.Fatalf("expected pid %d, got %d (%s, %s)", os.Getpid(), pid, name, path)
|
||||
}
|
||||
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if path == "" || name == "" {
|
||||
t.Fatalf("expected process path and name, got name=%q path=%q", name, path)
|
||||
}
|
||||
if sameFile(executable, path) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("expected executable %q, got %q", executable, path)
|
||||
}
|
||||
|
||||
func sameFile(left string, right string) bool {
|
||||
leftInfo, leftErr := os.Stat(left)
|
||||
rightInfo, rightErr := os.Stat(right)
|
||||
if leftErr != nil || rightErr != nil {
|
||||
return false
|
||||
}
|
||||
return os.SameFile(leftInfo, rightInfo)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinTCPWithoutDestination(t *testing.T) {
|
||||
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan stdnet.Conn, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
return
|
||||
}
|
||||
close(accepted)
|
||||
}()
|
||||
|
||||
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverConn := <-accepted
|
||||
if serverConn == nil {
|
||||
t.Fatal("server did not accept tcp connection")
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.TCPAddr)
|
||||
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinTCPWithDifferentDestination(t *testing.T) {
|
||||
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan stdnet.Conn, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
return
|
||||
}
|
||||
close(accepted)
|
||||
}()
|
||||
|
||||
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverConn := <-accepted
|
||||
if serverConn == nil {
|
||||
t.Fatal("server did not accept tcp connection")
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.TCPAddr)
|
||||
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "203.0.113.10", 443)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
|
||||
src := netip.MustParseAddr("198.18.0.2")
|
||||
dst := netip.MustParseAddr("203.0.113.10")
|
||||
otherLocal := netip.MustParseAddr("192.168.1.10")
|
||||
otherRemote := netip.MustParseAddr("198.51.100.10")
|
||||
unspecifiedLocal := netip.MustParseAddr("0.0.0.0")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
local netip.Addr
|
||||
remote netip.Addr
|
||||
hasDst bool
|
||||
wantLevel darwinSocketMatchLevel
|
||||
}{
|
||||
{
|
||||
name: "exact",
|
||||
local: src,
|
||||
remote: dst,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketExactMatch,
|
||||
},
|
||||
{
|
||||
name: "unspecified local with matching remote",
|
||||
local: unspecifiedLocal,
|
||||
remote: dst,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketExactMatch,
|
||||
},
|
||||
{
|
||||
name: "unspecified local without destination",
|
||||
local: unspecifiedLocal,
|
||||
remote: otherRemote,
|
||||
hasDst: false,
|
||||
wantLevel: darwinSocketExactMatch,
|
||||
},
|
||||
{
|
||||
name: "local match with different remote",
|
||||
local: src,
|
||||
remote: otherRemote,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketLocalMatch,
|
||||
},
|
||||
{
|
||||
name: "remote match with different local",
|
||||
local: otherLocal,
|
||||
remote: dst,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketRemoteMatch,
|
||||
},
|
||||
{
|
||||
name: "port only with destination",
|
||||
local: otherLocal,
|
||||
remote: otherRemote,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketPortMatch,
|
||||
},
|
||||
{
|
||||
name: "different local without destination",
|
||||
local: otherLocal,
|
||||
remote: otherRemote,
|
||||
hasDst: false,
|
||||
wantLevel: darwinSocketNoMatch,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
info := newDarwinSocketInfo("tcp", test.local, 12345, test.remote, 443)
|
||||
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, test.hasDst)
|
||||
if level != test.wantLevel {
|
||||
t.Fatalf("unexpected match level: got %d, want %d", level, test.wantLevel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
|
||||
info := make([]byte, darwinSocketFDInfoSize)
|
||||
switch network {
|
||||
case "tcp":
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_TCP))
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoTCP))
|
||||
case "udp":
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_UDP))
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoIN))
|
||||
}
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET))
|
||||
info[darwinInSockInfoVFlagOff] = darwinInSockInfoIPv4
|
||||
writeDarwinNativeUint32(info, darwinInSockInfoLPortOff, uint32(localPort))
|
||||
writeDarwinNativeUint32(info, darwinInSockInfoFPortOff, uint32(remotePort))
|
||||
copyDarwinIPv4(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], local)
|
||||
copyDarwinIPv4(info[darwinInSockInfoFAddrOff:darwinInSockInfoFAddrOff+16], remote)
|
||||
return info
|
||||
}
|
||||
|
||||
func writeDarwinNativeUint32(b []byte, offset int, value uint32) {
|
||||
*(*uint32)(unsafe.Pointer(&b[offset])) = value
|
||||
}
|
||||
|
||||
func copyDarwinIPv4(dst []byte, addr netip.Addr) {
|
||||
ip := addr.As4()
|
||||
copy(dst[12:16], ip[:])
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
|
||||
return 0, "", "", errors.New("process lookup is not supported on this platform")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !windows && !linux && !android && !darwin
|
||||
//go:build !windows && !linux && !android
|
||||
|
||||
package net
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var envReloadRegistry = struct {
|
||||
sync.RWMutex
|
||||
handlers []func() error
|
||||
}{}
|
||||
|
||||
// RegisterEnvReload registers an environment reload handler and runs it once
|
||||
// immediately so package defaults keep the same behavior as init-time reads.
|
||||
func RegisterEnvReload(handler func() error) {
|
||||
if handler == nil {
|
||||
return
|
||||
}
|
||||
envReloadRegistry.Lock()
|
||||
envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler)
|
||||
envReloadRegistry.Unlock()
|
||||
if err := handler(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadEnvSettings refreshes all registered environment-backed package state.
|
||||
func ReloadEnvSettings() error {
|
||||
envReloadRegistry.RLock()
|
||||
handlers := append([]func() error{}, envReloadRegistry.handlers...)
|
||||
envReloadRegistry.RUnlock()
|
||||
|
||||
var errs []error
|
||||
for _, handler := range handlers {
|
||||
if err := handler(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -143,9 +143,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(b) < hdrLen+4+block.BlockSize() {
|
||||
return nil, errNotQuic
|
||||
}
|
||||
|
||||
cache.Clear()
|
||||
mask := cache.Extend(int32(block.BlockSize()))
|
||||
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)])
|
||||
|
||||
@@ -70,7 +70,7 @@ type Outbound struct {
|
||||
Tag string
|
||||
// Name of the outbound proxy that handles the connection.
|
||||
Name string
|
||||
// Unused. Conn is actually internet.Connection. May be nil. It is currently nil for outbound with proxySettings
|
||||
// Unused. Conn is actually internet.Connection. May be nil.
|
||||
Conn net.Conn
|
||||
// CanSpliceCopy is a property for this connection
|
||||
// 1 = can, 2 = after processing protocol info should be able to, 3 = cannot
|
||||
|
||||
+19
-33
@@ -8,7 +8,7 @@ import (
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -27,39 +27,25 @@ var AddrParser = protocol.NewAddressParser(
|
||||
)
|
||||
|
||||
var (
|
||||
Show atomic.Bool
|
||||
baseKey atomic.Value
|
||||
Show bool
|
||||
BaseKey []byte
|
||||
)
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
Show.Store(strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true")
|
||||
raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" })
|
||||
if raw == "" {
|
||||
ensureBaseKey()
|
||||
return nil
|
||||
}
|
||||
key, _ := base64.RawURLEncoding.DecodeString(raw)
|
||||
if len(key) != 32 {
|
||||
return errors.New(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(key)))
|
||||
}
|
||||
baseKey.Store(append([]byte(nil), key...))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureBaseKey() []byte {
|
||||
if key := baseKey.Load(); key != nil {
|
||||
return key.([]byte)
|
||||
}
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
baseKey.Store(key)
|
||||
return key
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" {
|
||||
Show = true
|
||||
}
|
||||
BaseKey = make([]byte, 32)
|
||||
rand.Read(BaseKey)
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV
|
||||
if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" {
|
||||
if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 {
|
||||
return
|
||||
}
|
||||
panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey)))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func GetGlobalID(ctx context.Context) (globalID [8]byte) {
|
||||
@@ -68,10 +54,10 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
|
||||
}
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
|
||||
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
|
||||
h := blake3.New(8, ensureBaseKey())
|
||||
h := blake3.New(8, BaseKey)
|
||||
h.Write([]byte(inbound.Source.String()))
|
||||
copy(globalID[:], h.Sum(nil))
|
||||
if Show.Load() {
|
||||
if Show {
|
||||
errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 7
|
||||
Version_z byte = 28
|
||||
Version_y byte = 6
|
||||
Version_z byte = 27
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -187,9 +187,6 @@ func NewWithContext(ctx context.Context, config *Config) (*Instance, error) {
|
||||
}
|
||||
|
||||
func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
|
||||
if err := platform.ReloadEnvSettings(); err != nil {
|
||||
return true, errors.New("failed to reload environment settings").Base(err)
|
||||
}
|
||||
server.ctx = context.WithValue(server.ctx, "cone",
|
||||
platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true")
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package policy
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
@@ -83,41 +82,32 @@ func ManagerType() interface{} {
|
||||
return (*Manager)(nil)
|
||||
}
|
||||
|
||||
var defaultBufferSize atomic.Int32
|
||||
var defaultBufferSize int32
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
defaultBufferSize.Store(readDefaultBufferSize())
|
||||
return nil
|
||||
}
|
||||
|
||||
func readDefaultBufferSize() int32 {
|
||||
func init() {
|
||||
const defaultValue = -17
|
||||
size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue)
|
||||
|
||||
switch size {
|
||||
case 0:
|
||||
return -1 // For pipe to use unlimited size
|
||||
defaultBufferSize = -1 // For pipe to use unlimited size
|
||||
case defaultValue: // Env flag not defined. Use default values per CPU-arch.
|
||||
switch runtime.GOARCH {
|
||||
case "arm", "mips", "mipsle":
|
||||
return 0
|
||||
defaultBufferSize = 0
|
||||
case "arm64", "mips64", "mips64le":
|
||||
return 4 * 1024 // 4k cache for low-end devices
|
||||
defaultBufferSize = 4 * 1024 // 4k cache for low-end devices
|
||||
default:
|
||||
return 512 * 1024
|
||||
defaultBufferSize = 512 * 1024
|
||||
}
|
||||
default:
|
||||
return int32(size) * 1024 * 1024
|
||||
defaultBufferSize = int32(size) * 1024 * 1024
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
func defaultBufferPolicy() Buffer {
|
||||
return Buffer{
|
||||
PerConnection: defaultBufferSize.Load(),
|
||||
PerConnection: defaultBufferSize,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-21
@@ -81,8 +81,6 @@ type Manager interface {
|
||||
|
||||
// RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters.
|
||||
RegisterCounter(string) (Counter, error)
|
||||
// GetOrRegisterCounter returns the counter by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterCounter(string) (Counter, error)
|
||||
// UnregisterCounter unregisters a counter from the manager by its identifier.
|
||||
UnregisterCounter(string) error
|
||||
// GetCounter returns a counter by its identifier.
|
||||
@@ -93,8 +91,6 @@ type Manager interface {
|
||||
|
||||
// RegisterOnlineMap registers a new OnlineMap to the manager. The identifier string must not be empty, and unique among other OnlineMaps.
|
||||
RegisterOnlineMap(string) (OnlineMap, error)
|
||||
// GetOrRegisterOnlineMap returns the OnlineMap by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterOnlineMap(string) (OnlineMap, error)
|
||||
// UnregisterOnlineMap unregisters an OnlineMap from the manager by its identifier.
|
||||
UnregisterOnlineMap(string) error
|
||||
// GetOnlineMap returns an OnlineMap by its identifier.
|
||||
@@ -105,8 +101,6 @@ type Manager interface {
|
||||
|
||||
// RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels.
|
||||
RegisterChannel(string) (Channel, error)
|
||||
// GetOrRegisterChannel returns the channel by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterChannel(string) (Channel, error)
|
||||
// UnregisterChannel unregisters a channel from the manager by its identifier.
|
||||
UnregisterChannel(string) error
|
||||
// GetChannel returns a channel by its identifier.
|
||||
@@ -116,6 +110,36 @@ type Manager interface {
|
||||
GetAllOnlineUsers() []string
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter.
|
||||
func GetOrRegisterCounter(m Manager, name string) (Counter, error) {
|
||||
counter := m.GetCounter(name)
|
||||
if counter != nil {
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
return m.RegisterCounter(name)
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new OnlineMap.
|
||||
func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) {
|
||||
onlineMap := m.GetOnlineMap(name)
|
||||
if onlineMap != nil {
|
||||
return onlineMap, nil
|
||||
}
|
||||
|
||||
return m.RegisterOnlineMap(name)
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel.
|
||||
func GetOrRegisterChannel(m Manager, name string) (Channel, error) {
|
||||
channel := m.GetChannel(name)
|
||||
if channel != nil {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
return m.RegisterChannel(name)
|
||||
}
|
||||
|
||||
// ManagerType returns the type of Manager interface. Can be used to implement common.HasType.
|
||||
//
|
||||
// xray:api:stable
|
||||
@@ -136,11 +160,6 @@ func (NoopManager) RegisterCounter(string) (Counter, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter implements Manager.
|
||||
func (NoopManager) GetOrRegisterCounter(string) (Counter, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterCounter implements Manager.
|
||||
func (NoopManager) UnregisterCounter(string) error {
|
||||
return nil
|
||||
@@ -159,11 +178,6 @@ func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap implements Manager.
|
||||
func (NoopManager) GetOrRegisterOnlineMap(string) (OnlineMap, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterOnlineMap implements Manager.
|
||||
func (NoopManager) UnregisterOnlineMap(string) error {
|
||||
return nil
|
||||
@@ -182,11 +196,6 @@ func (NoopManager) RegisterChannel(string) (Channel, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel implements Manager.
|
||||
func (NoopManager) GetOrRegisterChannel(string) (Channel, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterChannel implements Manager.
|
||||
func (NoopManager) UnregisterChannel(string) error {
|
||||
return nil
|
||||
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pion/stun/v3 v3.1.6
|
||||
github.com/pires/go-proxyproto v0.15.0
|
||||
github.com/pires/go-proxyproto v0.14.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.5.1
|
||||
@@ -23,15 +23,15 @@ require (
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/grpc v1.82.1
|
||||
google.golang.org/grpc v1.82.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
@@ -52,10 +52,10 @@ require (
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -53,8 +53,8 @@ github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
|
||||
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
|
||||
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
|
||||
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
|
||||
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
|
||||
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||
github.com/pires/go-proxyproto v0.14.0 h1:2vIGIfVG8eVRsKF0xukEoeT5RWhDXxBU0uv6smLOKdI=
|
||||
github.com/pires/go-proxyproto v0.14.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
@@ -98,22 +98,22 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -121,21 +121,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -149,8 +149,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
|
||||
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRootEnvAppliesArbitraryValues(t *testing.T) {
|
||||
const (
|
||||
valueKey = "XRAY_TEST_CONFIG_ENV"
|
||||
emptyKey = "XRAY_TEST_CONFIG_EMPTY"
|
||||
)
|
||||
t.Setenv(valueKey, "before")
|
||||
t.Setenv(emptyKey, "before")
|
||||
|
||||
config := new(Config)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"env": {
|
||||
"XRAY_TEST_CONFIG_ENV": "configured",
|
||||
"XRAY_TEST_CONFIG_EMPTY": ""
|
||||
}
|
||||
}`), config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := config.Build(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := os.Getenv(valueKey); got != "configured" {
|
||||
t.Fatalf("env %q = %q, want %q", valueKey, got, "configured")
|
||||
}
|
||||
if got := os.Getenv(emptyKey); got != "" {
|
||||
t.Fatalf("env %q = %q, want empty", emptyKey, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvConfigOverride(t *testing.T) {
|
||||
base := EnvConfig{
|
||||
"ONE": "one",
|
||||
"TWO": "old",
|
||||
}
|
||||
override := EnvConfig{
|
||||
"TWO": "new",
|
||||
"THREE": "three",
|
||||
}
|
||||
base.Override(override)
|
||||
|
||||
want := map[string]string{
|
||||
"ONE": "one",
|
||||
"TWO": "new",
|
||||
"THREE": "three",
|
||||
}
|
||||
for key, value := range want {
|
||||
if got := base[key]; got != value {
|
||||
t.Fatalf("env %q = %q, want %q", key, got, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||
@@ -25,7 +22,6 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -71,7 +67,6 @@ var (
|
||||
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
||||
"fragment": func() interface{} { return new(FragmentMask) },
|
||||
"sudoku": func() interface{} { return new(Sudoku) },
|
||||
"xmc": func() interface{} { return new(XMC) },
|
||||
}, "type", "settings")
|
||||
|
||||
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
@@ -720,81 +715,6 @@ func (c *Xdns) Build() (proto.Message, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
type XMC struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Profiles []XMCProfile `json:"profiles"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type XMCProfile struct {
|
||||
// Resolve the UUID by username, then request the session profile with
|
||||
// unsigned=false. Client and server must use the same signed profile.
|
||||
Username string `json:"username"`
|
||||
UUID string `json:"uuid"`
|
||||
TexturesValue string `json:"texturesValue"`
|
||||
TexturesSignature string `json:"texturesSignature"`
|
||||
}
|
||||
|
||||
var xmcUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9_]{3,16}$`)
|
||||
|
||||
func (c *XMCProfile) Build() (*xmc.Profile, error) {
|
||||
if !xmcUsernamePattern.MatchString(c.Username) {
|
||||
return nil, fmt.Errorf("invalid minecraft profile username: %q", c.Username)
|
||||
}
|
||||
|
||||
profileUUID, err := googleuuid.Parse(c.UUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid minecraft profile UUID: %w", err)
|
||||
}
|
||||
if c.TexturesValue == "" || c.TexturesSignature == "" {
|
||||
return nil, fmt.Errorf("incomplete minecraft profile textures")
|
||||
}
|
||||
|
||||
return &xmc.Profile{
|
||||
Username: c.Username,
|
||||
Uuid: append([]byte(nil), profileUUID[:]...),
|
||||
TexturesValue: c.TexturesValue,
|
||||
TexturesSignature: c.TexturesSignature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *XMC) Build() (proto.Message, error) {
|
||||
if len(c.Profiles) == 0 {
|
||||
return nil, fmt.Errorf("minecraft profiles are required")
|
||||
}
|
||||
|
||||
if c.Password == "" {
|
||||
return nil, fmt.Errorf("empty password")
|
||||
}
|
||||
|
||||
rsaPrivateKey, err := xmc.DeriveRSAKey(c.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive minecraft rsa key: %w", err)
|
||||
}
|
||||
|
||||
rsaPublicKey, err := x509.MarshalPKIXPublicKey(&rsaPrivateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err)
|
||||
}
|
||||
|
||||
profiles := make([]*xmc.Profile, 0, len(c.Profiles))
|
||||
for i := range c.Profiles {
|
||||
profile, err := c.Profiles[i].Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build minecraft profile %d: %w", i, err)
|
||||
}
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
|
||||
return &xmc.Config{
|
||||
Password: c.Password,
|
||||
Hostname: c.Hostname,
|
||||
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
|
||||
RsaPublicKey: rsaPublicKey,
|
||||
Profiles: profiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Xicmp struct {
|
||||
DGRAM bool `json:"dgram"`
|
||||
IPs []string `json:"ips"`
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
)
|
||||
|
||||
func TestXMCBuildProfile(t *testing.T) {
|
||||
built, err := (&XMC{
|
||||
Password: "test-password",
|
||||
Profiles: []XMCProfile{
|
||||
{
|
||||
Username: "TestUser",
|
||||
UUID: "00112233-4455-6677-8899-aabbccddeeff",
|
||||
TexturesValue: "textures-value",
|
||||
TexturesSignature: "textures-signature",
|
||||
},
|
||||
},
|
||||
}).Build()
|
||||
if err != nil {
|
||||
t.Fatalf("build XMC config: %v", err)
|
||||
}
|
||||
config := built.(*xmc.Config)
|
||||
if len(config.Profiles) != 1 || len(config.Profiles[0].Uuid) != 16 {
|
||||
t.Fatalf("unexpected profiles: %+v", config.Profiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMCBuildRequiresProfile(t *testing.T) {
|
||||
_, err := (&XMC{Password: "test-password"}).Build()
|
||||
if err == nil || !strings.Contains(err.Error(), "profiles are required") {
|
||||
t.Fatalf("expected required profiles error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -308,21 +308,3 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
Tag string `json:"tag"`
|
||||
|
||||
// TransportLayerProxy: For compatibility.
|
||||
TransportLayerProxy bool `json:"transportLayer"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) {
|
||||
if v.Tag == "" {
|
||||
return nil, errors.New("Proxy tag is not set.")
|
||||
}
|
||||
return &internet.ProxyConfig{
|
||||
Tag: v.Tag,
|
||||
TransportLayerProxy: v.TransportLayerProxy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -450,8 +450,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
|
||||
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
|
||||
}
|
||||
if c.Xmux == (XmuxConfig{}) {
|
||||
c.Xmux.MaxConnections.From = 3
|
||||
c.Xmux.MaxConnections.To = 3
|
||||
c.Xmux.MaxConnections.From = 6
|
||||
c.Xmux.MaxConnections.To = 6
|
||||
c.Xmux.HMaxRequestTimes.From = 600
|
||||
c.Xmux.HMaxRequestTimes.To = 900
|
||||
c.Xmux.HMaxReusableSecs.From = 1800
|
||||
|
||||
@@ -113,10 +113,6 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
|
||||
config.MinClientVer[i] = byte(u)
|
||||
}
|
||||
}
|
||||
errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
} else {
|
||||
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
|
||||
errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
|
||||
}
|
||||
if c.MaxClientVer != "" {
|
||||
config.MaxClientVer = make([]byte, 3)
|
||||
@@ -161,10 +157,8 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
for _, sn := range config.ServerNames {
|
||||
sn = strings.ToLower(sn)
|
||||
if strings.HasSuffix(sn, ".ru") || strings.HasSuffix(sn, ".ir") || strings.HasSuffix(sn, ".cn") ||
|
||||
strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") || strings.Contains(sn, "microsoft") {
|
||||
errors.LogWarning(context.Background(), `REALITY: Choosing "`, sn, `" as the target will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") {
|
||||
errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-70
@@ -1,19 +1,12 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/xtls/xray-core/proxy/tun"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type TunConfig struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
MTU uint32 `json:"mtu"`
|
||||
Gateway []string `json:"gateway"`
|
||||
DNS []string `json:"dns"`
|
||||
@@ -25,7 +18,6 @@ type TunConfig struct {
|
||||
func (v *TunConfig) Build() (proto.Message, error) {
|
||||
config := &tun.Config{
|
||||
Name: v.Name,
|
||||
Desc: v.Desc,
|
||||
MTU: v.MTU,
|
||||
Gateway: v.Gateway,
|
||||
DNS: v.DNS,
|
||||
@@ -40,71 +32,10 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
if config.Name == "" {
|
||||
name, err := GetAvailableTunName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Name = name
|
||||
}
|
||||
if config.Desc == "" {
|
||||
config.Desc = "Wintun"
|
||||
config.Name = "xray0"
|
||||
}
|
||||
if config.MTU == 0 {
|
||||
config.MTU = 1500
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tunNamePrefix = "utun"
|
||||
minTunIndex = 10
|
||||
maxTunIndex = 1024
|
||||
)
|
||||
|
||||
func GetAvailableTunName() (string, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fail to get system interface information: %w", err)
|
||||
}
|
||||
|
||||
usedNames := make(map[string]struct{}, len(interfaces))
|
||||
for _, iface := range interfaces {
|
||||
usedNames[iface.Name] = struct{}{}
|
||||
}
|
||||
|
||||
startIndex, err := randomInt(minTunIndex, maxTunIndex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fail to generate valid tun name: %w", err)
|
||||
}
|
||||
|
||||
rangeSize := maxTunIndex - minTunIndex + 1
|
||||
|
||||
for offset := 0; offset < rangeSize; offset++ {
|
||||
index := minTunIndex + (startIndex-minTunIndex+offset)%rangeSize
|
||||
name := tunNamePrefix + strconv.Itoa(index)
|
||||
|
||||
if _, exists := usedNames[name]; !exists {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"no available TUN interface name in range %s%d-%s%d",
|
||||
tunNamePrefix,
|
||||
minTunIndex,
|
||||
tunNamePrefix,
|
||||
maxTunIndex,
|
||||
)
|
||||
}
|
||||
|
||||
func randomInt(min, max int) (int, error) {
|
||||
value, err := rand.Int(
|
||||
rand.Reader,
|
||||
big.NewInt(int64(max-min+1)),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return min + int(value.Int64()), nil
|
||||
}
|
||||
|
||||
+32
-57
@@ -3,7 +3,6 @@ package conf
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
core "github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
@@ -176,7 +176,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
receiverSettings.StreamSettings = ss
|
||||
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
|
||||
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
|
||||
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`)
|
||||
}
|
||||
}
|
||||
if c.SniffingConfig != nil {
|
||||
@@ -216,21 +216,11 @@ type OutboundDetourConfig struct {
|
||||
Tag string `json:"tag"`
|
||||
Settings *json.RawMessage `json:"settings"`
|
||||
StreamSetting *StreamConfig `json:"streamSettings"`
|
||||
ProxySettings *ProxyConfig `json:"proxySettings"`
|
||||
ProxySettings *json.RawMessage `json:"proxySettings"`
|
||||
MuxSettings *MuxConfig `json:"mux"`
|
||||
TargetStrategy string `json:"targetStrategy"`
|
||||
}
|
||||
|
||||
func (c *OutboundDetourConfig) checkChainProxyConfig() error {
|
||||
if c.StreamSetting == nil || c.ProxySettings == nil || c.StreamSetting.SocketSettings == nil {
|
||||
return nil
|
||||
}
|
||||
if len(c.ProxySettings.Tag) > 0 && len(c.StreamSetting.SocketSettings.DialerProxy) > 0 {
|
||||
return errors.New("proxySettings.tag is conflicted with sockopt.dialerProxy").AtWarning()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func requiresTransportSecurity(address *Address) bool {
|
||||
if address == nil || address.Address == nil {
|
||||
return false
|
||||
@@ -267,6 +257,10 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
if c.ProxySettings != nil {
|
||||
return nil, errors.PrintRemovedFeatureError(`outbound "proxySettings"`, `"streamSettings.sockopt.dialerProxy"`)
|
||||
}
|
||||
|
||||
senderSettings := &proxyman.SenderConfig{}
|
||||
switch strings.ToLower(c.TargetStrategy) {
|
||||
case "asis", "":
|
||||
@@ -294,9 +288,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
default:
|
||||
return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy)
|
||||
}
|
||||
if err := c.checkChainProxyConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.SendThrough != nil {
|
||||
address := ParseSendThough(c.SendThrough)
|
||||
@@ -322,26 +313,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
senderSettings.StreamSettings = ss
|
||||
}
|
||||
|
||||
if c.ProxySettings != nil {
|
||||
ps, err := c.ProxySettings.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid outbound detour proxy settings").Base(err)
|
||||
}
|
||||
if ps.TransportLayerProxy {
|
||||
if senderSettings.StreamSettings != nil {
|
||||
if senderSettings.StreamSettings.SocketSettings != nil {
|
||||
senderSettings.StreamSettings.SocketSettings.DialerProxy = ps.Tag
|
||||
} else {
|
||||
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{DialerProxy: ps.Tag}
|
||||
}
|
||||
} else {
|
||||
senderSettings.StreamSettings = &internet.StreamConfig{SocketSettings: &internet.SocketConfig{DialerProxy: ps.Tag}}
|
||||
}
|
||||
ps = nil
|
||||
}
|
||||
senderSettings.ProxySettings = ps
|
||||
}
|
||||
|
||||
if c.MuxSettings != nil {
|
||||
ms, err := c.MuxSettings.Build()
|
||||
if err != nil {
|
||||
@@ -366,6 +337,31 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
|
||||
}
|
||||
|
||||
if fc, ok := ts.(*freedom.Config); ok {
|
||||
if senderSettings.StreamSettings != nil &&
|
||||
senderSettings.StreamSettings.SocketSettings != nil &&
|
||||
senderSettings.StreamSettings.SocketSettings.AddressPortStrategy != internet.AddressPortStrategy_None {
|
||||
return nil, errors.New(`freedom outbound does not support "sockopt.addressPortStrategy"`)
|
||||
}
|
||||
|
||||
var strategy internet.DomainStrategy
|
||||
if strategy = senderSettings.TargetStrategy; strategy != internet.DomainStrategy_AS_IS {
|
||||
errors.LogWarning(context.Background(), `The "outbound.targetStrategy" setting is not supported directly by freedom and has been automatically migrated to "sockopt.domainStrategy" with no behavior change.`)
|
||||
senderSettings.TargetStrategy = internet.DomainStrategy_AS_IS
|
||||
} else if strategy = fc.DomainStrategy; strategy != internet.DomainStrategy_AS_IS {
|
||||
errors.LogWarning(context.Background(), `The "freedom.domainStrategy" setting is deprecated and will be removed. For compatibility, its value has been automatically migrated to "sockopt.domainStrategy". Please update your config before removal.`)
|
||||
}
|
||||
if strategy != internet.DomainStrategy_AS_IS {
|
||||
if senderSettings.StreamSettings == nil {
|
||||
senderSettings.StreamSettings = &internet.StreamConfig{}
|
||||
}
|
||||
if senderSettings.StreamSettings.SocketSettings == nil {
|
||||
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{}
|
||||
}
|
||||
senderSettings.StreamSettings.SocketSettings.DomainStrategy = strategy
|
||||
}
|
||||
}
|
||||
|
||||
return &core.OutboundHandlerConfig{
|
||||
SenderSettings: serial.ToTypedMessage(senderSettings),
|
||||
Tag: c.Tag,
|
||||
@@ -380,20 +376,11 @@ func (c *StatsConfig) Build() (*stats.Config, error) {
|
||||
return &stats.Config{}, nil
|
||||
}
|
||||
|
||||
type EnvConfig map[string]string
|
||||
|
||||
func (c EnvConfig) Override(o EnvConfig) {
|
||||
for key, value := range o {
|
||||
c[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Deprecated: Global transport config is no longer used
|
||||
// left for returning error
|
||||
Transport map[string]json.RawMessage `json:"transport"`
|
||||
|
||||
Env EnvConfig `json:"env"`
|
||||
LogConfig *LogConfig `json:"log"`
|
||||
RouterConfig *RouterConfig `json:"routing"`
|
||||
DNSConfig *DNSConfig `json:"dns"`
|
||||
@@ -449,12 +436,6 @@ func (c *Config) Override(o *Config, fn string) {
|
||||
if o.Transport != nil {
|
||||
c.Transport = o.Transport
|
||||
}
|
||||
if o.Env != nil {
|
||||
if c.Env == nil {
|
||||
c.Env = EnvConfig{}
|
||||
}
|
||||
c.Env.Override(o.Env)
|
||||
}
|
||||
if o.Policy != nil {
|
||||
c.Policy = o.Policy
|
||||
}
|
||||
@@ -530,12 +511,6 @@ func (c *Config) Override(o *Config, fn string) {
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *Config) Build() (*core.Config, error) {
|
||||
for key, value := range c.Env {
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
return nil, errors.New("failed to apply environment configuration").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := PostProcessConfigureFile(c); err != nil {
|
||||
return nil, errors.New("failed to post-process configuration file").Base(err)
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ The -confdir=dir flag sets a dir with multiple json config
|
||||
The -format=json flag sets the format of config files.
|
||||
Default "auto".
|
||||
|
||||
The config root env object sets process environment variables after all config
|
||||
files are parsed. Variables needed to locate or parse config files must be set
|
||||
in the process environment before Xray starts.
|
||||
|
||||
The -test flag tells Xray to test config files only,
|
||||
without launching the server.
|
||||
|
||||
|
||||
+98
-117
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
@@ -32,27 +31,19 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
useSplice atomic.Bool
|
||||
useSplice bool
|
||||
allNetworks [8]bool
|
||||
defaultBlockPrivateRule *FinalRule
|
||||
defaultBlockAllRule *FinalRule
|
||||
)
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
enabled := false
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
enabled = true
|
||||
}
|
||||
useSplice.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
h := new(Handler)
|
||||
if streamSettings, ok := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig); ok && streamSettings.SocketSettings != nil {
|
||||
h.resolveStrategy = streamSettings.SocketSettings.DomainStrategy
|
||||
h.usesDialerProxy = len(streamSettings.SocketSettings.DialerProxy) > 0
|
||||
}
|
||||
if err := core.RequireFeatures(ctx, func(pm policy.Manager) error {
|
||||
return h.Init(config.(*Config), pm)
|
||||
}); err != nil {
|
||||
@@ -61,7 +52,12 @@ func init() {
|
||||
return h, nil
|
||||
}))
|
||||
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
useSplice = true
|
||||
}
|
||||
|
||||
for i := range allNetworks {
|
||||
allNetworks[i] = true
|
||||
@@ -89,9 +85,11 @@ type FinalRule struct {
|
||||
|
||||
// Handler handles Freedom connections.
|
||||
type Handler struct {
|
||||
policyManager policy.Manager
|
||||
config *Config
|
||||
finalRules []*FinalRule
|
||||
policyManager policy.Manager
|
||||
config *Config
|
||||
finalRules []*FinalRule
|
||||
resolveStrategy internet.DomainStrategy
|
||||
usesDialerProxy bool
|
||||
}
|
||||
|
||||
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
|
||||
@@ -168,22 +166,6 @@ func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, defaultRule *FinalRule) bool {
|
||||
if !dialDest.Address.Family().IsDomain() {
|
||||
return false
|
||||
}
|
||||
if len(h.finalRules) > 0 {
|
||||
rule := h.finalRules[0]
|
||||
if rule.action == RuleAction_Allow && rule.network[dialDest.Network] && len(rule.port) == 0 && rule.ip == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if defaultRule != nil || len(h.finalRules) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule {
|
||||
for _, rule := range h.finalRules {
|
||||
if rule.Apply(network, address, port) {
|
||||
@@ -196,13 +178,6 @@ func (h *Handler) matchFinalRule(network net.Network, address net.Address, port
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction {
|
||||
if rule := h.matchFinalRule(network, address, port, defaultRule); rule != nil {
|
||||
return rule.action
|
||||
}
|
||||
return RuleAction_Allow
|
||||
}
|
||||
|
||||
// Init initializes the Handler with necessary parameters.
|
||||
func (h *Handler) Init(config *Config, pm policy.Manager) error {
|
||||
h.config = config
|
||||
@@ -237,6 +212,20 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
|
||||
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
|
||||
}
|
||||
|
||||
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
|
||||
delay := h.blockDelay(rule)
|
||||
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
|
||||
timer := time.AfterFunc(delay, func() {
|
||||
common.Interrupt(input)
|
||||
common.Interrupt(output)
|
||||
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
|
||||
})
|
||||
defer timer.Stop()
|
||||
defer common.Close(output)
|
||||
_ = buf.Copy(input, buf.Discard)
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidAddress(addr *net.IPOrDomain) bool {
|
||||
if addr == nil {
|
||||
return false
|
||||
@@ -284,61 +273,53 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
var conn stat.Connection
|
||||
var blockedDest *net.Destination
|
||||
var blockedRule *FinalRule
|
||||
firstResolve := true
|
||||
err := retry.ExponentialBackoff(5, 100).On(func() error {
|
||||
dialDest := destination
|
||||
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
|
||||
strategy := h.config.DomainStrategy
|
||||
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
|
||||
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
|
||||
}
|
||||
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
|
||||
if h.config.DomainStrategy.ForceIP() || h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) {
|
||||
return err
|
||||
if destination.Address.Family().IsDomain() {
|
||||
if defaultRule != nil || len(h.finalRules) > 0 {
|
||||
if strategy := h.resolveStrategy; strategy.HasStrategy() {
|
||||
ips, err := internet.LookupForIP(destination.Address.Domain(), strategy, outGateway)
|
||||
if err != nil { // non-force may still dial with system DNS
|
||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
|
||||
if strategy.ForceIP() {
|
||||
return err // retry
|
||||
}
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if addr := net.IPAddress(ip); addr != nil {
|
||||
if rule := h.matchFinalRule(destination.Network, addr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &destination
|
||||
blockedDest.Address = addr
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, destination.Address.Domain())
|
||||
if err != nil { // dialer may retry DNS
|
||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipAddr := net.IPAddress(addr.IP); ipAddr != nil {
|
||||
if rule := h.matchFinalRule(destination.Network, ipAddr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &destination
|
||||
blockedDest.Address = ipAddr
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dialDest = net.Destination{
|
||||
Network: dialDest.Network,
|
||||
Address: net.IPAddress(ips[dice.Roll(len(ips))]),
|
||||
Port: dialDest.Port,
|
||||
}
|
||||
errors.LogInfo(ctx, "dialing to ", dialDest)
|
||||
}
|
||||
} else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { // asis + domain + hasrules
|
||||
domain := dialDest.Address.Domain()
|
||||
var ips []net.IP
|
||||
if firstResolve {
|
||||
firstResolve = false
|
||||
supportIPv4, supportIPv6 := utils.CheckRoutes()
|
||||
if supportIPv4 {
|
||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip4", domain)
|
||||
}
|
||||
if len(ips) == 0 && supportIPv6 {
|
||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip6", domain)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return errors.New("failed to get IP address for domain ", domain)
|
||||
}
|
||||
} else {
|
||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip", domain)
|
||||
} else {
|
||||
if rule := h.matchFinalRule(destination.Network, destination.Address, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &destination
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
if len(ips) == 0 { // SRV/TXT, lookup failed
|
||||
return errors.New("failed to get IP address for domain ", domain)
|
||||
}
|
||||
if addr := net.IPAddress(ips[dice.Roll(len(ips))]); addr != nil {
|
||||
dialDest.Address = addr
|
||||
errors.LogInfo(ctx, "dialing to ", dialDest)
|
||||
}
|
||||
}
|
||||
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &dialDest
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
|
||||
rawConn, err := dialer.Dial(ctx, dialDest)
|
||||
rawConn, err := dialer.Dial(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -350,20 +331,21 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return errors.New("failed to open connection to ", destination).Base(err)
|
||||
}
|
||||
if blockedDest != nil {
|
||||
delay := h.blockDelay(blockedRule)
|
||||
errors.LogInfo(ctx, "blocked target: ", *blockedDest, ", blackholing connection for ", delay)
|
||||
timer := time.AfterFunc(delay, func() {
|
||||
common.Interrupt(input)
|
||||
common.Interrupt(output)
|
||||
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *blockedDest)
|
||||
})
|
||||
defer timer.Stop()
|
||||
defer common.Close(output)
|
||||
if err := buf.Copy(input, buf.Discard); err != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return h.blackhole(ctx, input, output, blockedRule, blockedDest)
|
||||
}
|
||||
if destination.Address.Family().IsDomain() && (defaultRule != nil || len(h.finalRules) > 0) {
|
||||
if h.usesDialerProxy {
|
||||
errors.LogInfo(ctx, "skipping final rule check for proxied remote endpoint, original target: ", destination)
|
||||
} else {
|
||||
// pre-check may fail or dialer may select another IP
|
||||
remoteDest := net.DestinationFromAddr(conn.RemoteAddr())
|
||||
if rule := h.matchFinalRule(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
conn.Close()
|
||||
return h.blackhole(ctx, input, output, rule, &remoteDest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
|
||||
version := byte(h.config.ProxyProtocol)
|
||||
srcAddr := inbound.Source.RawNetAddr()
|
||||
@@ -408,7 +390,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
writer = buf.NewWriter(conn)
|
||||
}
|
||||
} else {
|
||||
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination)
|
||||
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination, outGateway)
|
||||
if h.config.Noises != nil {
|
||||
errors.LogDebug(ctx, "NOISE", h.config.Noises)
|
||||
writer = &NoisePacketWriter{
|
||||
@@ -430,7 +412,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(plcy.Timeouts.UplinkOnly)
|
||||
if destination.Network == net.Network_TCP && useSplice.Load() && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
var writeConn net.Conn
|
||||
var inTimer *signal.ActivityTimer
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil {
|
||||
@@ -512,7 +494,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
}
|
||||
udpAddr := d.(*net.UDPAddr)
|
||||
sourceAddr := net.IPAddress(udpAddr.IP)
|
||||
if r.Handler.applyFinalRules(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule) == RuleAction_Block {
|
||||
if rule := r.Handler.matchFinalRule(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
continue
|
||||
}
|
||||
b.Resize(0, int32(n))
|
||||
@@ -537,7 +519,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
}
|
||||
|
||||
// DialDest means the dial target used in the dialer when creating conn
|
||||
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Writer {
|
||||
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination, outGateway net.Address) buf.Writer {
|
||||
iConn := conn
|
||||
statConn, ok := iConn.(*stat.CounterConnection)
|
||||
if ok {
|
||||
@@ -561,9 +543,8 @@ func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
|
||||
DefaultRule: defaultRule,
|
||||
UDPOverride: UDPOverride,
|
||||
ResolvedUDPAddr: resolvedUDPAddr,
|
||||
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
|
||||
OutGateway: outGateway,
|
||||
}
|
||||
|
||||
}
|
||||
return &buf.SequentialWriter{Writer: conn}
|
||||
}
|
||||
@@ -580,7 +561,7 @@ type PacketWriter struct {
|
||||
// Resulting in these packets being sent to many different IPs randomly
|
||||
// So, cache and keep the resolve result
|
||||
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
|
||||
LocalAddr net.Address
|
||||
OutGateway net.Address
|
||||
}
|
||||
|
||||
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
@@ -603,21 +584,21 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
|
||||
b.UDP.Address = ip
|
||||
} else {
|
||||
ShouldUseSystemResolver := true
|
||||
if w.Handler.config.DomainStrategy.HasStrategy() {
|
||||
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr)
|
||||
shouldUseSystemResolver := true
|
||||
if strategy := w.Handler.resolveStrategy; strategy.HasStrategy() {
|
||||
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), strategy, w.OutGateway)
|
||||
if err != nil {
|
||||
// drop packet if resolve failed when forceIP
|
||||
if w.Handler.config.DomainStrategy.ForceIP() {
|
||||
if strategy.ForceIP() {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
ip = net.IPAddress(ips[dice.Roll(len(ips))])
|
||||
ShouldUseSystemResolver = false
|
||||
shouldUseSystemResolver = false
|
||||
}
|
||||
}
|
||||
if ShouldUseSystemResolver {
|
||||
if shouldUseSystemResolver {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
|
||||
if err != nil {
|
||||
b.Release()
|
||||
@@ -631,7 +612,7 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.applyFinalRules(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule) == RuleAction_Block {
|
||||
if rule := w.matchFinalRule(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
+2
-6
@@ -31,17 +31,13 @@ Here is simple Xray config snippet to enable the inbound:
|
||||
"port": 0,
|
||||
"protocol": "tun",
|
||||
"settings": {
|
||||
"name": "utun10",
|
||||
"desc": "Wintun",
|
||||
"mtu": 1500
|
||||
"name": "xray0",
|
||||
"MTU": 1492
|
||||
}
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
|
||||
It is ignored on other platforms.
|
||||
|
||||
## SUPPORTED FEATURES
|
||||
|
||||
- IPv4 and IPv6
|
||||
|
||||
+4
-14
@@ -7,12 +7,11 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,7 +30,6 @@ type Config struct {
|
||||
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
|
||||
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
|
||||
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -115,18 +113,11 @@ func (x *Config) GetAutoOutboundsInterface() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetDesc() string {
|
||||
if x != nil {
|
||||
return x.Desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proxy_tun_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
@@ -135,8 +126,7 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
|
||||
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
|
||||
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -14,5 +14,4 @@ message Config {
|
||||
uint32 user_level = 5;
|
||||
repeated string auto_system_routing_table = 6;
|
||||
string auto_outbounds_interface = 7;
|
||||
string desc = 8;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
t.uplinkCounter = c
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
t.downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ var _ GVisorDevice = (*WindowsTun)(nil)
|
||||
// interface with the same name exist, it tried to be reused.
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
// instantiate wintun adapter
|
||||
adapter, err := open(options.Name, options.Desc)
|
||||
adapter, err := open(options.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -73,12 +73,12 @@ func NewTun(options *Config) (Tun, error) {
|
||||
return tun, nil
|
||||
}
|
||||
|
||||
func open(name, desc string) (*wintun.Adapter, error) {
|
||||
func open(name string) (*wintun.Adapter, error) {
|
||||
// generate a deterministic GUID from the adapter name
|
||||
id := md5.Sum([]byte(name))
|
||||
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
|
||||
// try to create adapter anew
|
||||
adapter, err := wintun.CreateAdapter(name, desc, guid)
|
||||
adapter, err := wintun.CreateAdapter(name, "Xray", guid)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"hash/crc64"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -219,17 +218,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return nil
|
||||
}
|
||||
|
||||
var enablePadding atomic.Bool
|
||||
var enablePadding = false
|
||||
|
||||
func shouldEnablePadding(s protocol.SecurityType) bool {
|
||||
return enablePadding.Load() || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
}
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
enablePadding.Store(paddingValue != defaultFlagValue)
|
||||
return nil
|
||||
return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -237,5 +229,10 @@ func init() {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
if paddingValue != defaultFlagValue {
|
||||
enablePadding = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
@@ -139,7 +139,13 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
ob.Name = "wireguard"
|
||||
ob.CanSpliceCopy = 3
|
||||
|
||||
if err := h.init(ctx); err != nil {
|
||||
if h.dev == nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.dev.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -252,11 +258,8 @@ func (h *Handler) Close() (err error) {
|
||||
func (h *Handler) init(ctx context.Context) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.tun == nil {
|
||||
return errors.New("closed")
|
||||
}
|
||||
if h.dev != nil {
|
||||
return h.dev.Up()
|
||||
return nil
|
||||
}
|
||||
resolveFunc := h.resolveLocal
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -82,9 +82,14 @@ func TestResolveIP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Tag: "direct",
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
DomainStrategy: internet.DomainStrategy_USE_IP,
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
SocketSettings: &internet.SocketConfig{
|
||||
DomainStrategy: internet.DomainStrategy_USE_IP,
|
||||
},
|
||||
},
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func TestPassiveConnection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
func TestDialerProxy(t *testing.T) {
|
||||
tcpServer := tcp.Server{
|
||||
MsgProcessor: xor,
|
||||
}
|
||||
@@ -187,8 +187,10 @@ func TestProxy(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
ProxySettings: &internet.ProxyConfig{
|
||||
Tag: "proxy",
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
SocketSettings: &internet.SocketConfig{
|
||||
DialerProxy: "proxy",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -218,7 +220,7 @@ func TestProxy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyOverKCP(t *testing.T) {
|
||||
func TestDialerProxyOverKCP(t *testing.T) {
|
||||
tcpServer := tcp.Server{
|
||||
MsgProcessor: xor,
|
||||
}
|
||||
@@ -321,11 +323,11 @@ func TestProxyOverKCP(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
ProxySettings: &internet.ProxyConfig{
|
||||
Tag: "proxy",
|
||||
},
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "mkcp",
|
||||
SocketSettings: &internet.SocketConfig{
|
||||
DialerProxy: "proxy",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -27,10 +27,9 @@ type task struct {
|
||||
}
|
||||
|
||||
var (
|
||||
conns chan *websocket.Conn
|
||||
server *http.Server
|
||||
currentAddr string
|
||||
mu sync.Mutex
|
||||
conns chan *websocket.Conn
|
||||
server *http.Server
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
var upgrader = &websocket.Upgrader{
|
||||
@@ -48,13 +47,8 @@ func Reload() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if addr == currentAddr && (addr == "" || server != nil) {
|
||||
return
|
||||
}
|
||||
|
||||
if server != nil {
|
||||
server.Close()
|
||||
server = nil
|
||||
}
|
||||
if HasBrowserDialer() {
|
||||
for len(conns) > 0 {
|
||||
@@ -66,7 +60,6 @@ func Reload() {
|
||||
}
|
||||
conns = nil
|
||||
}
|
||||
currentAddr = addr
|
||||
if addr != "" {
|
||||
token := uuid.New()
|
||||
csrfToken := token.String()
|
||||
@@ -227,8 +220,5 @@ func CheckOK(conn *websocket.Conn) error {
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(func() error {
|
||||
Reload()
|
||||
return nil
|
||||
})
|
||||
Reload()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type ConfigCreator func() interface{}
|
||||
|
||||
var globalTransportConfigCreatorCache = make(map[string]ConfigCreator)
|
||||
|
||||
var strategy = [][]byte{
|
||||
var strategy = [11][3]byte{
|
||||
// name strategy, prefer, fallback
|
||||
{0, 0, 0}, // AsIs none, /, /
|
||||
{1, 0, 0}, // UseIP use, both, none
|
||||
@@ -25,8 +25,6 @@ var strategy = [][]byte{
|
||||
{2, 6, 4}, // ForceIPv6v4 force, 6, 4
|
||||
}
|
||||
|
||||
const unknownProtocol = "unknown"
|
||||
|
||||
func RegisterProtocolConfigCreator(name string, creator ConfigCreator) error {
|
||||
if _, found := globalTransportConfigCreatorCache[name]; found {
|
||||
return errors.New("protocol ", name, " is already registered").AtError()
|
||||
@@ -91,10 +89,6 @@ func (c *StreamConfig) HasSecuritySettings() bool {
|
||||
return len(c.SecuritySettings) > 0
|
||||
}
|
||||
|
||||
func (c *ProxyConfig) HasTag() bool {
|
||||
return c != nil && len(c.Tag) > 0
|
||||
}
|
||||
|
||||
func (m SocketConfig_TProxyMode) IsEnabled() bool {
|
||||
return m != SocketConfig_Off
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func (x SocketConfig_TProxyMode) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use SocketConfig_TProxyMode.Descriptor instead.
|
||||
func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{6, 0}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{5, 0}
|
||||
}
|
||||
|
||||
type TransportConfig struct {
|
||||
@@ -582,58 +582,6 @@ func (x *QuicParams) GetMaxIncomingStreams() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
|
||||
TransportLayerProxy bool `protobuf:"varint,2,opt,name=transportLayerProxy,proto3" json:"transportLayerProxy,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProxyConfig) Reset() {
|
||||
*x = ProxyConfig{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProxyConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProxyConfig) ProtoMessage() {}
|
||||
|
||||
func (x *ProxyConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
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 ProxyConfig.ProtoReflect.Descriptor instead.
|
||||
func (*ProxyConfig) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *ProxyConfig) GetTag() string {
|
||||
if x != nil {
|
||||
return x.Tag
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProxyConfig) GetTransportLayerProxy() bool {
|
||||
if x != nil {
|
||||
return x.TransportLayerProxy
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type CustomSockopt struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
System string `protobuf:"bytes,1,opt,name=system,proto3" json:"system,omitempty"`
|
||||
@@ -648,7 +596,7 @@ type CustomSockopt struct {
|
||||
|
||||
func (x *CustomSockopt) Reset() {
|
||||
*x = CustomSockopt{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -660,7 +608,7 @@ func (x *CustomSockopt) String() string {
|
||||
func (*CustomSockopt) ProtoMessage() {}
|
||||
|
||||
func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -673,7 +621,7 @@ func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CustomSockopt.ProtoReflect.Descriptor instead.
|
||||
func (*CustomSockopt) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{5}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *CustomSockopt) GetSystem() string {
|
||||
@@ -753,7 +701,7 @@ type SocketConfig struct {
|
||||
|
||||
func (x *SocketConfig) Reset() {
|
||||
*x = SocketConfig{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[6]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -765,7 +713,7 @@ func (x *SocketConfig) String() string {
|
||||
func (*SocketConfig) ProtoMessage() {}
|
||||
|
||||
func (x *SocketConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[6]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -778,7 +726,7 @@ func (x *SocketConfig) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SocketConfig.ProtoReflect.Descriptor instead.
|
||||
func (*SocketConfig) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{6}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *SocketConfig) GetMark() int32 {
|
||||
@@ -940,7 +888,7 @@ type HappyEyeballsConfig struct {
|
||||
|
||||
func (x *HappyEyeballsConfig) Reset() {
|
||||
*x = HappyEyeballsConfig{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[7]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -952,7 +900,7 @@ func (x *HappyEyeballsConfig) String() string {
|
||||
func (*HappyEyeballsConfig) ProtoMessage() {}
|
||||
|
||||
func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[7]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -965,7 +913,7 @@ func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use HappyEyeballsConfig.ProtoReflect.Descriptor instead.
|
||||
func (*HappyEyeballsConfig) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{7}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *HappyEyeballsConfig) GetPrioritizeIpv6() bool {
|
||||
@@ -1040,10 +988,7 @@ const file_transport_internet_config_proto_rawDesc = "" +
|
||||
" \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
|
||||
"\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
|
||||
"\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x120\n" +
|
||||
"\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"Q\n" +
|
||||
"\vProxyConfig\x12\x10\n" +
|
||||
"\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" +
|
||||
"\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" +
|
||||
"\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"\x93\x01\n" +
|
||||
"\rCustomSockopt\x12\x16\n" +
|
||||
"\x06system\x18\x01 \x01(\tR\x06system\x12\x18\n" +
|
||||
"\anetwork\x18\x02 \x01(\tR\anetwork\x12\x14\n" +
|
||||
@@ -1127,7 +1072,7 @@ func file_transport_internet_config_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_transport_internet_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
|
||||
var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||
var file_transport_internet_config_proto_goTypes = []any{
|
||||
(DomainStrategy)(0), // 0: xray.transport.internet.DomainStrategy
|
||||
(AddressPortStrategy)(0), // 1: xray.transport.internet.AddressPortStrategy
|
||||
@@ -1136,28 +1081,27 @@ var file_transport_internet_config_proto_goTypes = []any{
|
||||
(*StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig
|
||||
(*UdpHop)(nil), // 5: xray.transport.internet.UdpHop
|
||||
(*QuicParams)(nil), // 6: xray.transport.internet.QuicParams
|
||||
(*ProxyConfig)(nil), // 7: xray.transport.internet.ProxyConfig
|
||||
(*CustomSockopt)(nil), // 8: xray.transport.internet.CustomSockopt
|
||||
(*SocketConfig)(nil), // 9: xray.transport.internet.SocketConfig
|
||||
(*HappyEyeballsConfig)(nil), // 10: xray.transport.internet.HappyEyeballsConfig
|
||||
(*serial.TypedMessage)(nil), // 11: xray.common.serial.TypedMessage
|
||||
(*net.IPOrDomain)(nil), // 12: xray.common.net.IPOrDomain
|
||||
(*CustomSockopt)(nil), // 7: xray.transport.internet.CustomSockopt
|
||||
(*SocketConfig)(nil), // 8: xray.transport.internet.SocketConfig
|
||||
(*HappyEyeballsConfig)(nil), // 9: xray.transport.internet.HappyEyeballsConfig
|
||||
(*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage
|
||||
(*net.IPOrDomain)(nil), // 11: xray.common.net.IPOrDomain
|
||||
}
|
||||
var file_transport_internet_config_proto_depIdxs = []int32{
|
||||
11, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage
|
||||
12, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain
|
||||
10, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage
|
||||
11, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain
|
||||
3, // 2: xray.transport.internet.StreamConfig.transport_settings:type_name -> xray.transport.internet.TransportConfig
|
||||
11, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage
|
||||
11, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
11, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
10, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage
|
||||
10, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
10, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
6, // 6: xray.transport.internet.StreamConfig.quic_params:type_name -> xray.transport.internet.QuicParams
|
||||
9, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig
|
||||
8, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig
|
||||
5, // 8: xray.transport.internet.QuicParams.udp_hop:type_name -> xray.transport.internet.UdpHop
|
||||
2, // 9: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode
|
||||
0, // 10: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||
8, // 11: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt
|
||||
7, // 11: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt
|
||||
1, // 12: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy
|
||||
10, // 13: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig
|
||||
9, // 13: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig
|
||||
14, // [14:14] is the sub-list for method output_type
|
||||
14, // [14:14] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
@@ -1176,7 +1120,7 @@ func file_transport_internet_config_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc)),
|
||||
NumEnums: 3,
|
||||
NumMessages: 8,
|
||||
NumMessages: 7,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -86,11 +86,6 @@ message QuicParams {
|
||||
int64 max_incoming_streams = 13;
|
||||
}
|
||||
|
||||
message ProxyConfig {
|
||||
string tag = 1;
|
||||
bool transportLayerProxy = 2;
|
||||
}
|
||||
|
||||
message CustomSockopt {
|
||||
string system = 1;
|
||||
string network = 2;
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
package xmc
|
||||
|
||||
// Copied from https://github.com/Tnze/go-mc/blob/539b4a3a7f030332eb58b8a946116ae7907630d2/net/CFB8/cfb8.go
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/subtle"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type cfb8 struct {
|
||||
c cipher.Block
|
||||
blockSize int
|
||||
ivPos int
|
||||
iv []byte
|
||||
de bool
|
||||
}
|
||||
|
||||
func newCFB8Decrypt(c cipher.Block, iv []byte) *cfb8 {
|
||||
return newCFB8(c, iv, true)
|
||||
}
|
||||
|
||||
func newCFB8Encrypt(c cipher.Block, iv []byte) *cfb8 {
|
||||
return newCFB8(c, iv, false)
|
||||
}
|
||||
|
||||
func newCFB8(c cipher.Block, iv []byte, de bool) *cfb8 {
|
||||
cp := make([]byte, len(iv)*3)
|
||||
copy(cp, iv)
|
||||
return &cfb8{
|
||||
c: c,
|
||||
blockSize: c.BlockSize(),
|
||||
iv: cp,
|
||||
de: de,
|
||||
}
|
||||
}
|
||||
|
||||
func (cf *cfb8) XORKeyStream(dst, src []byte) {
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
if len(dst) < len(src) {
|
||||
panic("cfb8: output smaller than input")
|
||||
}
|
||||
|
||||
// If dst and src does not overlap in first block size,
|
||||
// and the length of src is greater than 2*blockSize,
|
||||
// we can use an optimized implementation.
|
||||
if len(src) > cf.blockSize<<1 &&
|
||||
(uintptr(unsafe.Pointer(&dst[0]))+uintptr(cf.blockSize) <= uintptr(unsafe.Pointer(&src[0])) ||
|
||||
uintptr(unsafe.Pointer(&src[0]))+uintptr(len(src)) <= uintptr(unsafe.Pointer(&dst[0]))) {
|
||||
// encrypt/decrypt first blockSize bytes
|
||||
// After this, the IV will come to the same as
|
||||
// the last blockSize of ciphertext, so
|
||||
// we can reuse them without copy.
|
||||
cf.xorKeyStream(dst, src[:cf.blockSize])
|
||||
var ciphertext []byte
|
||||
if cf.de {
|
||||
ciphertext = src
|
||||
} else {
|
||||
ciphertext = dst
|
||||
}
|
||||
dst = dst[cf.blockSize:]
|
||||
src = src[cf.blockSize:]
|
||||
iv := cf.iv
|
||||
_ = iv[0] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
var (
|
||||
i int
|
||||
val byte
|
||||
)
|
||||
dst = dst[:len(src)]
|
||||
if cf.de && // and requires to be non-overlapping at all
|
||||
uintptr(unsafe.Pointer(&dst[0])) <= uintptr(unsafe.Pointer(&src[len(src)-1])) &&
|
||||
uintptr(unsafe.Pointer(&src[0])) <= uintptr(unsafe.Pointer(&dst[len(dst)-1])) {
|
||||
for i = 0; i < len(src)-cf.blockSize; i += 1 {
|
||||
cf.c.Encrypt(dst[i:], ciphertext[i:])
|
||||
}
|
||||
subtle.XORBytes(dst, src[:i], dst)
|
||||
for ; i < len(src); i += 1 {
|
||||
cf.c.Encrypt(iv, ciphertext[i:])
|
||||
dst[i] = src[i] ^ iv[0]
|
||||
}
|
||||
} else {
|
||||
_ = ciphertext[len(src)]
|
||||
for i, val = range src {
|
||||
cf.c.Encrypt(iv, ciphertext[i:])
|
||||
dst[i] = val ^ iv[0]
|
||||
}
|
||||
// for-range does not increase i in the last loop,
|
||||
// compared to the classic for clause
|
||||
i += 1
|
||||
}
|
||||
// copy the current IV for next operation
|
||||
copy(iv, ciphertext[i:i+cf.blockSize])
|
||||
cf.ivPos = 0
|
||||
return
|
||||
}
|
||||
|
||||
cf.xorKeyStream(dst, src)
|
||||
}
|
||||
|
||||
func (cf *cfb8) xorKeyStream(dst, src []byte) {
|
||||
dst = dst[:len(src)] // remove bounds check in loop
|
||||
for i, val := range src {
|
||||
posPlusBlockSize := cf.ivPos + cf.blockSize
|
||||
// fast mod; 2*blockSize must be a non-negative integer power of 2
|
||||
tempPos := posPlusBlockSize & (cf.blockSize<<1 - 1)
|
||||
// reuse space to store encrypted block
|
||||
cf.c.Encrypt(cf.iv[tempPos:], cf.iv[cf.ivPos:])
|
||||
// Only the first byte of the encrypted block is used
|
||||
// for encryption/decryption, other bytes are ignored.
|
||||
val ^= cf.iv[tempPos]
|
||||
|
||||
if cf.ivPos == cf.blockSize<<1 {
|
||||
// bound reached; move to next round for next operation
|
||||
// copy next block to the start of the ring buffer
|
||||
copy(cf.iv, cf.iv[cf.ivPos+1:])
|
||||
// insert the encrypted byte to the end of IV
|
||||
if cf.de {
|
||||
cf.iv[cf.blockSize-1] = src[i]
|
||||
} else {
|
||||
cf.iv[cf.blockSize-1] = val
|
||||
}
|
||||
cf.ivPos = 0
|
||||
} else {
|
||||
// insert the encrypted byte to the end of IV
|
||||
if cf.de {
|
||||
cf.iv[posPlusBlockSize] = src[i]
|
||||
} else {
|
||||
cf.iv[posPlusBlockSize] = val
|
||||
}
|
||||
// move to next block
|
||||
cf.ivPos += 1
|
||||
}
|
||||
|
||||
dst[i] = val
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type clientConn struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
c net.Conn
|
||||
|
||||
state clientState
|
||||
|
||||
handshakeLock sync.Mutex
|
||||
lifecycleMu sync.Mutex
|
||||
closed bool
|
||||
profiles []loginProfile
|
||||
password string
|
||||
rsaPublicKey []byte
|
||||
hostname string
|
||||
paddingSchedule []paddingTurn
|
||||
packet *packetStream
|
||||
deadlines *connectionDeadlines
|
||||
}
|
||||
|
||||
type clientState int
|
||||
|
||||
var (
|
||||
clientStateHandshake clientState = 1
|
||||
clientStateProxy clientState = 2
|
||||
)
|
||||
|
||||
func newClientConn(c net.Conn, profiles []loginProfile, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
|
||||
if len(rsaPublicKey) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa public key")
|
||||
}
|
||||
if len(profiles) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
paddingSchedule, err := newClientPaddingSchedule2612()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select padding profile: %w", err)
|
||||
}
|
||||
return &clientConn{
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: clientStateHandshake,
|
||||
handshakeLock: sync.Mutex{},
|
||||
profiles: profiles,
|
||||
password: password,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
hostname: hostname,
|
||||
paddingSchedule: paddingSchedule,
|
||||
deadlines: newConnectionDeadlines(c),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *clientConn) handshake() error {
|
||||
c.handshakeLock.Lock()
|
||||
defer c.handshakeLock.Unlock()
|
||||
|
||||
if c.state != clientStateHandshake {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.deadlines.beginHandshake(); err != nil {
|
||||
return fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
defer func() { _ = c.deadlines.endHandshake() }()
|
||||
|
||||
var (
|
||||
protocolVersion Varint = Varint(775)
|
||||
serverAddress String = String(c.hostname)
|
||||
serverPort UnsignedShort = UnsignedShort(25565)
|
||||
nextState Varint = Varint(2)
|
||||
)
|
||||
|
||||
host, portString, err := net.SplitHostPort(c.c.RemoteAddr().String())
|
||||
if err == nil {
|
||||
port, err := strconv.Atoi(portString)
|
||||
if err == nil {
|
||||
serverPort = UnsignedShort(port)
|
||||
}
|
||||
|
||||
if serverAddress == "" {
|
||||
serverAddress = String(host)
|
||||
}
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write handshake packet: %w", err)
|
||||
}
|
||||
|
||||
// Login Start
|
||||
randomProfile, err := rand.Int(rand.Reader, big.NewInt(int64(len(c.profiles))))
|
||||
if err != nil {
|
||||
return fmt.Errorf("select profile: %w", err)
|
||||
}
|
||||
selectedProfile := c.profiles[randomProfile.Int64()]
|
||||
username := String(selectedProfile.Username)
|
||||
|
||||
err = writePacket(c.writer, 0x00, &username, &selectedProfile.UUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login start: %w", err)
|
||||
}
|
||||
|
||||
// Encryption Request
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encryption request: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0x01 {
|
||||
return fmt.Errorf("bad encrypt request packet id")
|
||||
}
|
||||
|
||||
var (
|
||||
serverId String
|
||||
publicKey Bytes
|
||||
verifyToken Bytes
|
||||
)
|
||||
|
||||
err = pkt.readFields(&serverId, &publicKey, &verifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encryption request fields: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(publicKey, c.rsaPublicKey) {
|
||||
return fmt.Errorf("server public key mismatch")
|
||||
}
|
||||
|
||||
k, err := x509.ParsePKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse server public key: %w", err)
|
||||
}
|
||||
|
||||
rsaPublicKey, ok := k.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("parse server public key: not rsa")
|
||||
}
|
||||
|
||||
sharedSecret := make([]byte, 16)
|
||||
if _, err = rand.Read(sharedSecret); err != nil {
|
||||
return fmt.Errorf("generate shared secret: %w", err)
|
||||
}
|
||||
|
||||
encryptedSharedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt shared secret: %w", err)
|
||||
}
|
||||
|
||||
verifyToken = append(verifyToken, []byte(c.password)...) // append pre-shared password
|
||||
|
||||
encryptedVerifyToken, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, verifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt verify token: %w", err)
|
||||
}
|
||||
|
||||
// Send Encryption Response
|
||||
err = writePacket(
|
||||
c.writer,
|
||||
0x01,
|
||||
(*Bytes)(&encryptedSharedSecret),
|
||||
(*Bytes)(&encryptedVerifyToken),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write encryption response: %w", err)
|
||||
}
|
||||
|
||||
// Enable encryption
|
||||
c.reader, err = newCryptoReader(c.reader, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto reader: %w", err)
|
||||
}
|
||||
|
||||
c.writer, err = newCryptoWriter(c.writer, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto writer: %w", err)
|
||||
}
|
||||
|
||||
pkt, err = readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished: %w", err)
|
||||
}
|
||||
if pkt.packetID == 0x00 {
|
||||
var reason String
|
||||
if readErr := pkt.readFields(&reason); readErr != nil {
|
||||
return fmt.Errorf("authentication rejected")
|
||||
}
|
||||
return fmt.Errorf("authentication rejected: %s", reason)
|
||||
}
|
||||
if pkt.packetID != 0x02 {
|
||||
return fmt.Errorf("bad login finished packet id: %d", pkt.packetID)
|
||||
}
|
||||
|
||||
receivedProfile, err := readLoginSuccess(pkt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished fields: %w", err)
|
||||
}
|
||||
if receivedProfile != selectedProfile {
|
||||
return fmt.Errorf("login profile mismatch")
|
||||
}
|
||||
loginAcknowledgedLength, err := writePacketWithLength(c.writer, 0x03)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login acknowledged: %w", err)
|
||||
}
|
||||
if err = runPaddingSchedule(c.reader, c.writer, true, loginAcknowledgedLength, c.paddingSchedule); err != nil {
|
||||
return fmt.Errorf("run startup padding: %w", err)
|
||||
}
|
||||
|
||||
packet := newPacketStream(c.reader, c.writer, true)
|
||||
c.lifecycleMu.Lock()
|
||||
if c.closed {
|
||||
c.lifecycleMu.Unlock()
|
||||
packet.Stop()
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.packet = packet
|
||||
c.reader = packet
|
||||
c.writer = packet
|
||||
c.state = clientStateProxy
|
||||
c.lifecycleMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientConn) Read(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.reader.Read(b)
|
||||
}
|
||||
|
||||
func (c *clientConn) Write(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.writer.Write(b)
|
||||
}
|
||||
|
||||
func (c *clientConn) Close() error {
|
||||
c.lifecycleMu.Lock()
|
||||
c.closed = true
|
||||
packet := c.packet
|
||||
c.lifecycleMu.Unlock()
|
||||
if packet != nil {
|
||||
packet.Stop()
|
||||
}
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c *clientConn) LocalAddr() net.Addr {
|
||||
return c.c.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *clientConn) RemoteAddr() net.Addr {
|
||||
return c.c.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *clientConn) SetDeadline(t time.Time) error {
|
||||
return c.deadlines.setDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientConn) SetReadDeadline(t time.Time) error {
|
||||
return c.deadlines.setReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.deadlines.setWriteDeadline(t)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) TCP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := newClientConn(conn, profiles, c.Password, c.RsaPublicKey, c.Hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := wrapConnServer(conn, profiles, c.Password, c.RsaPrivateKey, c.RsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: transport/internet/finalmask/xmc/config.proto
|
||||
|
||||
package xmc
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Uuid []byte `protobuf:"bytes,2,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
TexturesValue string `protobuf:"bytes,3,opt,name=textures_value,json=texturesValue,proto3" json:"textures_value,omitempty"`
|
||||
TexturesSignature string `protobuf:"bytes,4,opt,name=textures_signature,json=texturesSignature,proto3" json:"textures_signature,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Profile) Reset() {
|
||||
*x = Profile{}
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Profile) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Profile) ProtoMessage() {}
|
||||
|
||||
func (x *Profile) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_xmc_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 Profile.ProtoReflect.Descriptor instead.
|
||||
func (*Profile) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Profile) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetUuid() []byte {
|
||||
if x != nil {
|
||||
return x.Uuid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesValue() string {
|
||||
if x != nil {
|
||||
return x.TexturesValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesSignature() string {
|
||||
if x != nil {
|
||||
return x.TexturesSignature
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
|
||||
RsaPrivateKey []byte `protobuf:"bytes,8,opt,name=rsa_private_key,json=rsaPrivateKey,proto3" json:"rsa_private_key,omitempty"`
|
||||
RsaPublicKey []byte `protobuf:"bytes,9,opt,name=rsa_public_key,json=rsaPublicKey,proto3" json:"rsa_public_key,omitempty"`
|
||||
Hostname string `protobuf:"bytes,10,opt,name=hostname,proto3" json:"hostname,omitempty"`
|
||||
Profiles []*Profile `protobuf:"bytes,11,rep,name=profiles,proto3" json:"profiles,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_finalmask_xmc_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_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Config) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetRsaPrivateKey() []byte {
|
||||
if x != nil {
|
||||
return x.RsaPrivateKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetRsaPublicKey() []byte {
|
||||
if x != nil {
|
||||
return x.RsaPublicKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetHostname() string {
|
||||
if x != nil {
|
||||
return x.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetProfiles() []*Profile {
|
||||
if x != nil {
|
||||
return x.Profiles
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_finalmask_xmc_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_finalmask_xmc_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"-transport/internet/finalmask/xmc/config.proto\x12%xray.transport.internet.finalmask.xmc\"\x8f\x01\n" +
|
||||
"\aProfile\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x12\n" +
|
||||
"\x04uuid\x18\x02 \x01(\fR\x04uuid\x12%\n" +
|
||||
"\x0etextures_value\x18\x03 \x01(\tR\rtexturesValue\x12-\n" +
|
||||
"\x12textures_signature\x18\x04 \x01(\tR\x11texturesSignature\"\xe0\x01\n" +
|
||||
"\x06Config\x12\x1a\n" +
|
||||
"\bpassword\x18\x01 \x01(\tR\bpassword\x12&\n" +
|
||||
"\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" +
|
||||
"\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKey\x12\x1a\n" +
|
||||
"\bhostname\x18\n" +
|
||||
" \x01(\tR\bhostname\x12J\n" +
|
||||
"\bprofiles\x18\v \x03(\v2..xray.transport.internet.finalmask.xmc.ProfileR\bprofilesJ\x04\b\x02\x10\x03B\x91\x01\n" +
|
||||
")com.xray.transport.internet.finalmask.xmcP\x01Z:github.com/xtls/xray-core/transport/internet/finalmask/xmc\xaa\x02%Xray.Transport.Internet.Finalmask.XMCb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce sync.Once
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP() []byte {
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce.Do(func() {
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)))
|
||||
})
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_finalmask_xmc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_transport_internet_finalmask_xmc_config_proto_goTypes = []any{
|
||||
(*Profile)(nil), // 0: xray.transport.internet.finalmask.xmc.Profile
|
||||
(*Config)(nil), // 1: xray.transport.internet.finalmask.xmc.Config
|
||||
}
|
||||
var file_transport_internet_finalmask_xmc_config_proto_depIdxs = []int32{
|
||||
0, // 0: xray.transport.internet.finalmask.xmc.Config.profiles:type_name -> xray.transport.internet.finalmask.xmc.Profile
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_xmc_config_proto_init() }
|
||||
func file_transport_internet_finalmask_xmc_config_proto_init() {
|
||||
if File_transport_internet_finalmask_xmc_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_finalmask_xmc_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_finalmask_xmc_config_proto_depIdxs,
|
||||
MessageInfos: file_transport_internet_finalmask_xmc_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_finalmask_xmc_config_proto = out.File
|
||||
file_transport_internet_finalmask_xmc_config_proto_goTypes = nil
|
||||
file_transport_internet_finalmask_xmc_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.transport.internet.finalmask.xmc;
|
||||
option csharp_namespace = "Xray.Transport.Internet.Finalmask.XMC";
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/xmc";
|
||||
option java_package = "com.xray.transport.internet.finalmask.xmc";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Profile {
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
string username = 1;
|
||||
bytes uuid = 2;
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
string textures_value = 3;
|
||||
string textures_signature = 4;
|
||||
}
|
||||
|
||||
message Config {
|
||||
string password = 1;
|
||||
reserved 2;
|
||||
bytes rsa_private_key = 8;
|
||||
bytes rsa_public_key = 9;
|
||||
string hostname = 10;
|
||||
repeated Profile profiles = 11;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const handshakeTimeout = 2 * time.Minute
|
||||
|
||||
type connectionDeadlines struct {
|
||||
mu sync.Mutex
|
||||
c net.Conn
|
||||
|
||||
read time.Time
|
||||
write time.Time
|
||||
handshake time.Time
|
||||
}
|
||||
|
||||
func newConnectionDeadlines(c net.Conn) *connectionDeadlines {
|
||||
return &connectionDeadlines{c: c}
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) beginHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Now().Add(handshakeTimeout)
|
||||
if err := d.applyLocked(); err != nil {
|
||||
d.handshake = time.Time{}
|
||||
_ = d.applyLocked()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) endHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Time{}
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
d.write = t
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setReadDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
return d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setWriteDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.write = t
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) applyLocked() error {
|
||||
if err := d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake)); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func earlierDeadline(user, internal time.Time) time.Time {
|
||||
if internal.IsZero() {
|
||||
return user
|
||||
}
|
||||
if user.IsZero() || internal.Before(user) {
|
||||
return internal
|
||||
}
|
||||
return user
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConnectionDeadlinesRestoreCallerValues(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
recording := &deadlineRecordingConn{Conn: client}
|
||||
deadlines := newConnectionDeadlines(recording)
|
||||
callerDeadline := time.Now().Add(10 * time.Minute)
|
||||
if err := deadlines.setDeadline(callerDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := deadlines.beginHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
read, write := recording.currentDeadlines()
|
||||
if !read.Before(callerDeadline) || !write.Before(callerDeadline) {
|
||||
t.Fatalf("handshake deadlines = %s/%s, caller = %s", read, write, callerDeadline)
|
||||
}
|
||||
|
||||
shortReadDeadline := time.Now().Add(time.Second)
|
||||
if err := deadlines.setReadDeadline(shortReadDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, _ = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) {
|
||||
t.Fatalf("read deadline = %s, want %s", read, shortReadDeadline)
|
||||
}
|
||||
|
||||
if err := deadlines.endHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, write = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) || !write.Equal(callerDeadline) {
|
||||
t.Fatalf("restored deadlines = %s/%s, want %s/%s", read, write, shortReadDeadline, callerDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
type deadlineRecordingConn struct {
|
||||
net.Conn
|
||||
mu sync.Mutex
|
||||
read time.Time
|
||||
write time.Time
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetReadDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetWriteDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) currentDeadlines() (time.Time, time.Time) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.read, c.write
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
type sha256Stream struct {
|
||||
seed []byte
|
||||
counter uint64
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newSHA256Stream(seed []byte) *sha256Stream {
|
||||
return &sha256Stream{
|
||||
seed: seed,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sha256Stream) Read(p []byte) (n int, err error) {
|
||||
for len(p) > len(s.buf) {
|
||||
h := sha256.New()
|
||||
h.Write(s.seed)
|
||||
h.Write([]byte(fmt.Sprintf("-%d", s.counter)))
|
||||
s.counter++
|
||||
s.buf = append(s.buf, h.Sum(nil)...)
|
||||
}
|
||||
n = copy(p, s.buf)
|
||||
s.buf = s.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func derivePrime(stream *sha256Stream) *big.Int {
|
||||
pBytes := make([]byte, 64) // 512 bits
|
||||
_, _ = stream.Read(pBytes)
|
||||
pBytes[0] |= 0xc0 // ensure it is big enough so p*q is 1024 bits
|
||||
pBytes[63] |= 0x01 // ensure odd
|
||||
|
||||
p := new(big.Int).SetBytes(pBytes)
|
||||
for {
|
||||
if p.ProbablyPrime(20) {
|
||||
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
e := big.NewInt(65537)
|
||||
gcd := new(big.Int).GCD(nil, nil, pMinus1, e)
|
||||
if gcd.Cmp(big.NewInt(1)) == 0 {
|
||||
return p
|
||||
}
|
||||
}
|
||||
p.Add(p, big.NewInt(2))
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveRSAKey derives a 1024-bit RSA private key from a password.
|
||||
func DeriveRSAKey(password string) (*rsa.PrivateKey, error) {
|
||||
seed := []byte(password)
|
||||
|
||||
pStream := newSHA256Stream(append(seed, []byte("-p-prime")...))
|
||||
qStream := newSHA256Stream(append(seed, []byte("-q-prime")...))
|
||||
|
||||
p := derivePrime(pStream)
|
||||
q := derivePrime(qStream)
|
||||
|
||||
// ensure p != q (if they are, let's search q further)
|
||||
for p.Cmp(q) == 0 {
|
||||
q.Add(q, big.NewInt(2))
|
||||
for {
|
||||
if q.ProbablyPrime(20) {
|
||||
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
e := big.NewInt(65537)
|
||||
gcd := new(big.Int).GCD(nil, nil, qMinus1, e)
|
||||
if gcd.Cmp(big.NewInt(1)) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
q.Add(q, big.NewInt(2))
|
||||
}
|
||||
}
|
||||
|
||||
n := new(big.Int).Mul(p, q)
|
||||
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
totient := new(big.Int).Mul(pMinus1, qMinus1)
|
||||
|
||||
e := big.NewInt(65537)
|
||||
d := new(big.Int).ModInverse(e, totient)
|
||||
if d == nil {
|
||||
return nil, fmt.Errorf("failed to compute mod inverse")
|
||||
}
|
||||
|
||||
priv := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: n,
|
||||
E: 65537,
|
||||
},
|
||||
D: d,
|
||||
Primes: []*big.Int{p, q},
|
||||
}
|
||||
priv.Precompute()
|
||||
|
||||
return priv, nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeriveRSAKey(t *testing.T) {
|
||||
password := "my-very-secret-password-12345"
|
||||
|
||||
key1, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
err = key1.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("key is not valid RSA key: %v", err)
|
||||
}
|
||||
|
||||
key2, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key second time: %v", err)
|
||||
}
|
||||
|
||||
// Verify determinism
|
||||
if key1.D.Cmp(key2.D) != 0 || key1.N.Cmp(key2.N) != 0 {
|
||||
t.Errorf("derived keys are not identical for the same password")
|
||||
}
|
||||
|
||||
// Verify different passwords yield different keys
|
||||
keyDifferent, err := DeriveRSAKey(password + "-different")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive different key: %v", err)
|
||||
}
|
||||
|
||||
if key1.D.Cmp(keyDifferent.D) == 0 || key1.N.Cmp(keyDifferent.N) == 0 {
|
||||
t.Errorf("derived keys are identical for different passwords")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveRSAKeyGoldenPrivateKey(t *testing.T) {
|
||||
const password = "deterministic-rsa-key-golden"
|
||||
const wantPKCS1DERHash = "3a8c4ad56a6fb42dab73c4d5fc3af754460a2db1441edc0970cbc7f4e0798d2f"
|
||||
|
||||
key, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
gotHash := sha256.Sum256(x509.MarshalPKCS1PrivateKey(key))
|
||||
got := hex.EncodeToString(gotHash[:])
|
||||
if got != wantPKCS1DERHash {
|
||||
t.Fatalf("derived private key changed\nwant sha256: %s\n got sha256: %s", wantPKCS1DERHash, got)
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func deriveTestRSAKey(t *testing.T, password string) ([]byte, []byte) {
|
||||
t.Helper()
|
||||
|
||||
key, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive rsa key: %v", err)
|
||||
}
|
||||
|
||||
publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal public key: %v", err)
|
||||
}
|
||||
|
||||
return x509.MarshalPKCS1PrivateKey(key), publicKey
|
||||
}
|
||||
|
||||
func testLoginProfile(username string) loginProfile {
|
||||
profile := loginProfile{
|
||||
Username: username,
|
||||
TexturesValue: strings.Repeat("texture-value-", 40),
|
||||
TexturesSignature: strings.Repeat("texture-signature-", 24),
|
||||
}
|
||||
digest := sha256.Sum256([]byte(username))
|
||||
copy(profile.UUID[:], digest[:16])
|
||||
profile.UUID[6] = (profile.UUID[6] & 0x0f) | 0x40
|
||||
profile.UUID[8] = (profile.UUID[8] & 0x3f) | 0x80
|
||||
return profile
|
||||
}
|
||||
|
||||
func TestHandshakeSuccess(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
password := "super-secure-shared-key-12345"
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
|
||||
go func() {
|
||||
rawConn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to wrap server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := server.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("server read error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:n], []byte("hello server")) {
|
||||
t.Errorf("unexpected payload from client: %s", string(buf[:n]))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = server.Write([]byte("hello client"))
|
||||
if err != nil {
|
||||
t.Errorf("server write error: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
clientRaw, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Write([]byte("hello server"))
|
||||
if err != nil {
|
||||
t.Fatalf("client write error: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := client.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("client read error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:n], []byte("hello client")) {
|
||||
t.Errorf("unexpected payload from server: %s", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandshakePasswordMismatch(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
clientPassword := "client-secret-123"
|
||||
serverPassword := "server-secret-456"
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
rawConn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, profiles, serverPassword, serverPrivateKey, serverPublicKey)
|
||||
if err != nil {
|
||||
// Wrapping is synchronous and shouldn't fail initially simply because key derivation works with any string
|
||||
t.Logf("wrapped server: %v", err)
|
||||
}
|
||||
|
||||
// When client sends data, handshake happens and should fail
|
||||
buf := make([]byte, 1024)
|
||||
_, err = server.Read(buf)
|
||||
if err == nil {
|
||||
t.Errorf("expected handshake to fail due to password mismatch, but it succeeded")
|
||||
} else {
|
||||
t.Logf("server read failed as expected: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
clientRaw, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, clientPassword, serverPublicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.handshake()
|
||||
if err == nil {
|
||||
t.Fatal("expected client handshake to fail due to password mismatch")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check if we lost connection or received error
|
||||
t.Log("Handshake mismatch tested")
|
||||
}
|
||||
|
||||
func TestHandshakeNetPipeWithKeepAlive(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "net-pipe-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("pipe_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
|
||||
request := make([]byte, len("hello server"))
|
||||
if _, err = io.ReadFull(server, request); err != nil {
|
||||
serverDone <- fmt.Errorf("read request: %w", err)
|
||||
return
|
||||
}
|
||||
if string(request) != "hello server" {
|
||||
serverDone <- fmt.Errorf("unexpected request: %q", request)
|
||||
return
|
||||
}
|
||||
|
||||
followupDone := make(chan error, 1)
|
||||
go func() {
|
||||
followup := make([]byte, len("after keepalive"))
|
||||
_, readErr := io.ReadFull(server, followup)
|
||||
if readErr == nil && string(followup) != "after keepalive" {
|
||||
readErr = fmt.Errorf("unexpected followup: %q", followup)
|
||||
}
|
||||
followupDone <- readErr
|
||||
}()
|
||||
|
||||
if err = server.packet.writeKeepAlive(Long(42)); err != nil {
|
||||
serverDone <- fmt.Errorf("write keep-alive: %w", err)
|
||||
return
|
||||
}
|
||||
if _, err = server.Write([]byte("hello client")); err != nil {
|
||||
serverDone <- fmt.Errorf("write response: %w", err)
|
||||
return
|
||||
}
|
||||
serverDone <- <-followupDone
|
||||
}()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write([]byte("hello server")); err != nil {
|
||||
t.Fatalf("write request: %v", err)
|
||||
}
|
||||
response := make([]byte, len("hello client"))
|
||||
if _, err = io.ReadFull(client, response); err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
}
|
||||
if string(response) != "hello client" {
|
||||
t.Fatalf("unexpected response: %q", response)
|
||||
}
|
||||
if _, err = client.Write([]byte("after keepalive")); err != nil {
|
||||
t.Fatalf("write followup: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("net.Pipe handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusQueryUnaffected(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "status-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("status_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err == nil {
|
||||
err = server.handshake()
|
||||
}
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
protocolVersion := Varint(775)
|
||||
serverAddress := String("localhost")
|
||||
serverPort := UnsignedShort(25565)
|
||||
nextState := Varint(1)
|
||||
if err := writePacket(clientRaw, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writePacket(clientRaw, 0x00); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.packetID != 0x00 {
|
||||
t.Fatalf("status packet id = %d", response.packetID)
|
||||
}
|
||||
var responseJSON String
|
||||
if err = response.readFields(&responseJSON); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(responseJSON) != statusResponse {
|
||||
t.Fatalf("status response = %q", responseJSON)
|
||||
}
|
||||
|
||||
payload := Long(0x0102030405060708)
|
||||
if err = writePacket(clientRaw, 0x01, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pong, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var receivedPayload Long
|
||||
if pong.packetID != 0x01 {
|
||||
t.Fatalf("pong packet id = %d", pong.packetID)
|
||||
}
|
||||
if err = pong.readFields(&receivedPayload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if receivedPayload != payload {
|
||||
t.Fatalf("pong payload = %x", receivedPayload)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err == nil || !strings.Contains(err.Error(), "ping") {
|
||||
t.Fatalf("server error = %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("status handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHandshakeHonorsCallerDeadline(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "deadline-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("deadline_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = client.SetDeadline(time.Now().Add(30 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
_, err = client.Write([]byte("blocked"))
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
|
||||
t.Fatalf("caller deadline took %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCloseInterruptsHandshake(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "close-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("close_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, writeErr := client.Write([]byte("blocked"))
|
||||
done <- writeErr
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if err = client.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-done:
|
||||
if err == nil {
|
||||
t.Fatal("handshake unexpectedly succeeded after close")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("close did not interrupt handshake")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLoginAcknowledgedPacketRejectsData(t *testing.T) {
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03}); err != nil {
|
||||
t.Fatalf("valid login acknowledged packet: %v", err)
|
||||
}
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03, data: []byte{0x00}}); err == nil {
|
||||
t.Fatal("login acknowledged packet with trailing data was accepted")
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
configurationClientboundCustomPayload = 0x01
|
||||
configurationServerboundCustomPayload = 0x02
|
||||
configurationKeepAlive = 0x04
|
||||
|
||||
packetChannel = "xmc:data"
|
||||
maxPacketData = 24 * 1024
|
||||
keepAlivePeriod = 15 * time.Second
|
||||
)
|
||||
|
||||
// packetStream carries the raw proxy byte stream in Minecraft configuration
|
||||
// custom payload packets. The configuration state provides bidirectional
|
||||
// payload packets and keep-alives without requiring version-specific world data.
|
||||
type packetStream struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
isClient bool
|
||||
|
||||
readMu sync.Mutex
|
||||
writeMu sync.Mutex
|
||||
pending []byte
|
||||
|
||||
keepAliveID atomic.Int64
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newPacketStream(reader io.Reader, writer io.Writer, isClient bool) *packetStream {
|
||||
s := &packetStream{
|
||||
reader: reader,
|
||||
writer: writer,
|
||||
isClient: isClient,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
if !isClient {
|
||||
go s.keepAliveLoop()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *packetStream) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.readMu.Lock()
|
||||
defer s.readMu.Unlock()
|
||||
|
||||
if len(s.pending) > 0 {
|
||||
n := copy(p, s.pending)
|
||||
s.pending = s.pending[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for {
|
||||
packet, err := readPacket(s.reader)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read minecraft packet stream: %w", err)
|
||||
}
|
||||
|
||||
if packet.packetID == s.remoteCustomPayloadID() {
|
||||
payload, ok, err := parseCustomPayload(packet)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok || len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
n := copy(p, payload)
|
||||
if n < len(payload) {
|
||||
s.pending = append(s.pending[:0], payload[n:]...)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if packet.packetID == configurationKeepAlive {
|
||||
var id Long
|
||||
if err := packet.readFields(&id); err != nil {
|
||||
return 0, fmt.Errorf("read minecraft keep-alive: %w", err)
|
||||
}
|
||||
if s.isClient {
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *packetStream) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
|
||||
written := 0
|
||||
for written < len(p) {
|
||||
end := written + maxPacketData
|
||||
if end > len(p) {
|
||||
end = len(p)
|
||||
}
|
||||
channel := String(packetChannel)
|
||||
payload := RestBytes(p[written:end])
|
||||
if err := writePacket(s.writer, s.localCustomPayloadID(), &channel, &payload); err != nil {
|
||||
return written, fmt.Errorf("write minecraft custom payload: %w", err)
|
||||
}
|
||||
written = end
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) Stop() {
|
||||
s.stopOnce.Do(func() { close(s.done) })
|
||||
}
|
||||
|
||||
func (s *packetStream) localCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
|
||||
func (s *packetStream) remoteCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
|
||||
func parseCustomPayload(packet *mcPacket) ([]byte, bool, error) {
|
||||
r := bytes.NewReader(packet.data)
|
||||
var channel String
|
||||
if err := channel.readFrom(r); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload channel: %w", err)
|
||||
}
|
||||
if string(channel) != packetChannel {
|
||||
return nil, false, nil
|
||||
}
|
||||
payload := make([]byte, r.Len())
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload data: %w", err)
|
||||
}
|
||||
return payload, true, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) writeKeepAlive(id Long) error {
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if err := writePacket(s.writer, configurationKeepAlive, &id); err != nil {
|
||||
return fmt.Errorf("write minecraft keep-alive: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *packetStream) keepAliveLoop() {
|
||||
ticker := time.NewTicker(keepAlivePeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
id := Long(s.keepAliveID.Add(1))
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return
|
||||
}
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketStreamUsesPlainFraming(t *testing.T) {
|
||||
payload := []byte("hello")
|
||||
var wire bytes.Buffer
|
||||
stream := newPacketStream(bytes.NewReader(nil), &wire, true)
|
||||
|
||||
written, err := stream.Write(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
if written != len(payload) {
|
||||
t.Fatalf("written = %d, want %d", written, len(payload))
|
||||
}
|
||||
wantOutbound := []byte{0x0f, 0x02, 0x08, 'x', 'm', 'c', ':', 'd', 'a', 't', 'a', 'h', 'e', 'l', 'l', 'o'}
|
||||
if !bytes.Equal(wire.Bytes(), wantOutbound) {
|
||||
t.Fatalf("wire frame = %x, want %x", wire.Bytes(), wantOutbound)
|
||||
}
|
||||
|
||||
wantInbound := append([]byte(nil), wantOutbound...)
|
||||
wantInbound[1] = configurationClientboundCustomPayload
|
||||
reader := newPacketStream(bytes.NewReader(wantInbound), io.Discard, true)
|
||||
got := make([]byte, len(payload))
|
||||
if _, err = io.ReadFull(reader, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("payload = %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketStreamRoundTrip(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
const password = "packet-stream-shared-key"
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
profiles := []loginProfile{testLoginProfile("packet_user")}
|
||||
clientPayload := bytes.Repeat([]byte("client-payload-"), 5000)
|
||||
serverPayload := bytes.Repeat([]byte("server-payload-"), 5000)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
rawConn, acceptErr := ln.Accept()
|
||||
if acceptErr != nil {
|
||||
serverDone <- acceptErr
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, wrapErr := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if wrapErr != nil {
|
||||
serverDone <- wrapErr
|
||||
return
|
||||
}
|
||||
got := make([]byte, len(clientPayload))
|
||||
if _, readErr := io.ReadFull(server, got); readErr != nil {
|
||||
serverDone <- readErr
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, clientPayload) {
|
||||
serverDone <- io.ErrUnexpectedEOF
|
||||
return
|
||||
}
|
||||
_, writeErr := server.Write(serverPayload)
|
||||
serverDone <- writeErr
|
||||
}()
|
||||
|
||||
rawClient, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rawClient.Close()
|
||||
|
||||
client, err := newClientConn(rawClient, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write(clientPayload); err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
got := make([]byte, len(serverPayload))
|
||||
if _, err = io.ReadFull(client, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, serverPayload) {
|
||||
t.Fatal("server payload mismatch")
|
||||
}
|
||||
if err = <-serverDone; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
type paddingDirection uint8
|
||||
|
||||
const (
|
||||
paddingClientToServer paddingDirection = iota + 1
|
||||
paddingServerToClient
|
||||
|
||||
paddingBufferLength = 16 * 1024
|
||||
maxPaddingChunkLength = 48 * 1024
|
||||
maxPaddingTurnLength = 8 * 1024 * 1024
|
||||
)
|
||||
|
||||
type paddingVariant struct {
|
||||
chunks []int
|
||||
delays []paddingDelayRange
|
||||
}
|
||||
|
||||
type paddingDelayRange struct {
|
||||
min time.Duration
|
||||
max time.Duration
|
||||
}
|
||||
|
||||
type paddingTurn struct {
|
||||
direction paddingDirection
|
||||
minLength int
|
||||
maxLength int
|
||||
variants []paddingVariant
|
||||
startDelay paddingDelayRange
|
||||
chunkDelay paddingDelayRange
|
||||
writeChunkMinLength int
|
||||
writeChunkLength int
|
||||
sendMinLength int
|
||||
sendMaxLength int
|
||||
sendVariants []int
|
||||
}
|
||||
|
||||
func runPaddingSchedule(reader io.Reader, writer io.Writer, isClient bool, firstTurnPrefixLength int, schedule []paddingTurn) error {
|
||||
if err := validatePaddingSchedule(schedule, firstTurnPrefixLength); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var writeBuffer []byte
|
||||
for i, turn := range schedule {
|
||||
prefixLength := 0
|
||||
if i == 0 {
|
||||
prefixLength = firstTurnPrefixLength
|
||||
}
|
||||
|
||||
localSends := isClient == (turn.direction == paddingClientToServer)
|
||||
if localSends {
|
||||
if err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {
|
||||
return fmt.Errorf("write padding turn %d: %w", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := readPaddingTurn(reader, turn, prefixLength); err != nil {
|
||||
return fmt.Errorf("read padding turn %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {
|
||||
if len(schedule) == 0 {
|
||||
return fmt.Errorf("empty padding schedule")
|
||||
}
|
||||
if firstTurnPrefixLength < 0 {
|
||||
return fmt.Errorf("negative first turn prefix length: %d", firstTurnPrefixLength)
|
||||
}
|
||||
if firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {
|
||||
return fmt.Errorf("first prefixed padding turn is not client-to-server")
|
||||
}
|
||||
|
||||
for i, turn := range schedule {
|
||||
if turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {
|
||||
return fmt.Errorf("padding turn %d has invalid direction: %d", i, turn.direction)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.startDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid start delay: %w", i, err)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.chunkDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid chunk delay: %w", i, err)
|
||||
}
|
||||
if turn.writeChunkMinLength < 0 || turn.writeChunkLength < turn.writeChunkMinLength || turn.writeChunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid write chunk range: %d-%d", i, turn.writeChunkMinLength, turn.writeChunkLength)
|
||||
}
|
||||
if len(turn.variants) > 0 && turn.writeChunkLength != 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with generated write chunks", i)
|
||||
}
|
||||
|
||||
minLength, maxLength, err := paddingTurnBounds(turn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("padding turn %d: %w", i, err)
|
||||
}
|
||||
hasSendRange := turn.sendMinLength != 0 || turn.sendMaxLength != 0
|
||||
if hasSendRange {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with a send range", i)
|
||||
}
|
||||
if turn.sendMinLength < minLength || turn.sendMaxLength < turn.sendMinLength || turn.sendMaxLength > maxLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid send range: %d-%d", i, turn.sendMinLength, turn.sendMaxLength)
|
||||
}
|
||||
}
|
||||
if i == 0 && minLength-firstTurnPrefixLength < 1 {
|
||||
return fmt.Errorf("padding turn 0 is too short for %d prefix bytes", firstTurnPrefixLength)
|
||||
}
|
||||
if i == 0 && len(turn.variants) > 0 {
|
||||
for j, variant := range turn.variants {
|
||||
if _, _, err = trimPaddingPrefix(variant, firstTurnPrefixLength); err != nil {
|
||||
return fmt.Errorf("padding turn 0 variant %d: %w", j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if i > 0 && turn.direction == schedule[i-1].direction {
|
||||
return fmt.Errorf("padding turns %d and %d have the same direction", i-1, i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writePaddingTurn(w io.Writer, turn paddingTurn, prefixLength int) error {
|
||||
return writePaddingTurnWithSleep(w, turn, prefixLength, time.Sleep)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithSleep(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration)) error {
|
||||
return writePaddingTurnWithBuffer(w, turn, prefixLength, sleep, nil)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithBuffer(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration), reusableBuffer *[]byte) error {
|
||||
startDelay, err := randomPaddingDelay(turn.startDelay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding start delay: %w", err)
|
||||
}
|
||||
if startDelay > 0 {
|
||||
sleep(startDelay)
|
||||
}
|
||||
|
||||
targetLength, chunks, delays, err := selectPaddingVariant(turn, prefixLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordLength := targetLength - prefixLength
|
||||
if recordLength < 1 {
|
||||
return fmt.Errorf("target length %d leaves an invalid record length %d", targetLength, recordLength)
|
||||
}
|
||||
|
||||
encodedLength := Varint(recordLength)
|
||||
var header bytes.Buffer
|
||||
if err = encodedLength.writeTo(&header); err != nil {
|
||||
return fmt.Errorf("write padding header: %w", err)
|
||||
}
|
||||
if len(chunks) == 0 {
|
||||
writeChunkLength := turn.writeChunkLength
|
||||
if writeChunkLength == 0 {
|
||||
writeChunkLength = paddingBufferLength
|
||||
} else if turn.writeChunkMinLength > 0 {
|
||||
writeChunkLength, err = randomPaddingTarget(turn.writeChunkMinLength, writeChunkLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding write chunk length: %w", err)
|
||||
}
|
||||
}
|
||||
chunks = defaultPaddingChunks(recordLength, writeChunkLength)
|
||||
delays = make([]paddingDelayRange, len(chunks))
|
||||
for i := 1; i < len(delays); i++ {
|
||||
delays[i] = turn.chunkDelay
|
||||
}
|
||||
}
|
||||
if chunks[0] < header.Len() {
|
||||
return fmt.Errorf("first padding chunk %d is shorter than header %d", chunks[0], header.Len())
|
||||
}
|
||||
|
||||
maxChunkLength := 0
|
||||
for _, chunkLength := range chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("invalid padding chunk length: %d", chunkLength)
|
||||
}
|
||||
maxChunkLength = max(maxChunkLength, chunkLength)
|
||||
}
|
||||
var buffer []byte
|
||||
if reusableBuffer == nil {
|
||||
buffer = make([]byte, maxChunkLength)
|
||||
} else {
|
||||
if cap(*reusableBuffer) < maxChunkLength {
|
||||
*reusableBuffer = make([]byte, maxChunkLength)
|
||||
}
|
||||
buffer = (*reusableBuffer)[:maxChunkLength]
|
||||
clear(buffer)
|
||||
}
|
||||
copy(buffer, header.Bytes())
|
||||
written := 0
|
||||
for i, chunkLength := range chunks {
|
||||
if i < len(delays) {
|
||||
delay, delayErr := randomPaddingDelay(delays[i])
|
||||
if delayErr != nil {
|
||||
return fmt.Errorf("select padding chunk %d delay: %w", i, delayErr)
|
||||
}
|
||||
if delay > 0 {
|
||||
sleep(delay)
|
||||
}
|
||||
}
|
||||
if err = writeFull(w, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("write padding chunk %d: %w", i, err)
|
||||
}
|
||||
written += chunkLength
|
||||
if i == 0 {
|
||||
clear(buffer[:header.Len()])
|
||||
}
|
||||
}
|
||||
if written != recordLength {
|
||||
return fmt.Errorf("padding chunks total %d, want %d", written, recordLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPaddingTurn(r io.Reader, turn paddingTurn, prefixLength int) error {
|
||||
encodedLength, headerLength, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read padding header: %w", err)
|
||||
}
|
||||
recordLength := int(encodedLength)
|
||||
if recordLength < headerLength || recordLength > maxPaddingTurnLength {
|
||||
return fmt.Errorf("invalid padding record length: %d", recordLength)
|
||||
}
|
||||
totalLength := prefixLength + recordLength
|
||||
if !paddingTurnAcceptsLength(turn, totalLength) {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn length %d is not an allowed variant", totalLength)
|
||||
}
|
||||
return fmt.Errorf("padding turn length %d is outside %d-%d", totalLength, turn.minLength, turn.maxLength)
|
||||
}
|
||||
|
||||
var buffer [paddingBufferLength]byte
|
||||
remaining := recordLength - headerLength
|
||||
for remaining > 0 {
|
||||
chunkLength := min(remaining, len(buffer))
|
||||
if _, err := io.ReadFull(r, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("read padding body: %w", err)
|
||||
}
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectPaddingVariant(turn paddingTurn, prefixLength int) (int, []int, []paddingDelayRange, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
minimum, maximum := turn.minLength, turn.maxLength
|
||||
if turn.sendMinLength != 0 || turn.sendMaxLength != 0 {
|
||||
minimum, maximum = turn.sendMinLength, turn.sendMaxLength
|
||||
}
|
||||
targetLength, err := randomPaddingTarget(minimum, maximum)
|
||||
return targetLength, nil, nil, err
|
||||
}
|
||||
|
||||
indices := turn.sendVariants
|
||||
if len(indices) == 0 {
|
||||
indices = make([]int, len(turn.variants))
|
||||
for i := range indices {
|
||||
indices[i] = i
|
||||
}
|
||||
}
|
||||
selected, err := randomPaddingIndex(len(indices))
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
variantIndex := indices[selected]
|
||||
if variantIndex < 0 || variantIndex >= len(turn.variants) {
|
||||
return 0, nil, nil, fmt.Errorf("invalid send variant index: %d", variantIndex)
|
||||
}
|
||||
variant := turn.variants[variantIndex]
|
||||
targetLength := paddingVariantLength(variant)
|
||||
chunks, delays, err := trimPaddingPrefix(variant, prefixLength)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
return targetLength, chunks, delays, nil
|
||||
}
|
||||
|
||||
func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
|
||||
remainingPrefix := prefixLength
|
||||
firstChunk := 0
|
||||
for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
|
||||
chunkLength := variant.chunks[firstChunk]
|
||||
if remainingPrefix < chunkLength {
|
||||
return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
|
||||
}
|
||||
remainingPrefix -= chunkLength
|
||||
firstChunk++
|
||||
}
|
||||
if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
|
||||
return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
|
||||
}
|
||||
|
||||
chunks := append([]int(nil), variant.chunks[firstChunk:]...)
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
if len(variant.delays) > 0 {
|
||||
copy(delays, variant.delays[firstChunk:])
|
||||
}
|
||||
return chunks, delays, nil
|
||||
}
|
||||
|
||||
func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
|
||||
chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
|
||||
for remaining := recordLength; remaining > 0; {
|
||||
chunkLength := min(remaining, writeChunkLength)
|
||||
chunks = append(chunks, chunkLength)
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func paddingTurnBounds(turn paddingTurn) (int, int, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
if turn.minLength < 1 || turn.maxLength < turn.minLength || turn.maxLength > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("invalid range: %d-%d", turn.minLength, turn.maxLength)
|
||||
}
|
||||
return turn.minLength, turn.maxLength, nil
|
||||
}
|
||||
if turn.minLength != 0 || turn.maxLength != 0 {
|
||||
return 0, 0, fmt.Errorf("variants cannot be combined with a length range")
|
||||
}
|
||||
|
||||
minLength := maxPaddingTurnLength + 1
|
||||
maxLength := 0
|
||||
for i, variant := range turn.variants {
|
||||
if len(variant.chunks) == 0 {
|
||||
return 0, 0, fmt.Errorf("variant %d has no chunks", i)
|
||||
}
|
||||
if len(variant.delays) != 0 && len(variant.delays) != len(variant.chunks) {
|
||||
return 0, 0, fmt.Errorf("variant %d has %d chunks and %d delays", i, len(variant.chunks), len(variant.delays))
|
||||
}
|
||||
for j, chunkLength := range variant.chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has invalid length: %d", i, j, chunkLength)
|
||||
}
|
||||
if len(variant.delays) > 0 {
|
||||
if err := validatePaddingDelayRange(variant.delays[j]); err != nil {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has an invalid delay: %w", i, j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
length := paddingVariantLength(variant)
|
||||
if length > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("variant %d is too long: %d", i, length)
|
||||
}
|
||||
minLength = min(minLength, length)
|
||||
maxLength = max(maxLength, length)
|
||||
}
|
||||
for _, index := range turn.sendVariants {
|
||||
if index < 0 || index >= len(turn.variants) {
|
||||
return 0, 0, fmt.Errorf("invalid send variant index: %d", index)
|
||||
}
|
||||
}
|
||||
return minLength, maxLength, nil
|
||||
}
|
||||
|
||||
func paddingTurnAcceptsLength(turn paddingTurn, length int) bool {
|
||||
if len(turn.variants) == 0 {
|
||||
return length >= turn.minLength && length <= turn.maxLength
|
||||
}
|
||||
for _, variant := range turn.variants {
|
||||
if paddingVariantLength(variant) == length {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func paddingVariantLength(variant paddingVariant) int {
|
||||
total := 0
|
||||
for _, chunkLength := range variant.chunks {
|
||||
total += chunkLength
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func validatePaddingDelayRange(delay paddingDelayRange) error {
|
||||
if delay.min < 0 || delay.max < delay.min {
|
||||
return fmt.Errorf("invalid range: %s-%s", delay.min, delay.max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomPaddingDelay(delay paddingDelayRange) (time.Duration, error) {
|
||||
if err := validatePaddingDelayRange(delay); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if delay.min == delay.max {
|
||||
return delay.min, nil
|
||||
}
|
||||
span := int64(delay.max-delay.min) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding delay: %w", err)
|
||||
}
|
||||
return delay.min + time.Duration(offset.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingIndex(length int) (int, error) {
|
||||
if length < 1 {
|
||||
return 0, fmt.Errorf("select from empty padding choices")
|
||||
}
|
||||
if length == 1 {
|
||||
return 0, nil
|
||||
}
|
||||
index, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding choice: %w", err)
|
||||
}
|
||||
return int(index.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingTarget(minLength, maxLength int) (int, error) {
|
||||
if minLength == maxLength {
|
||||
return minLength, nil
|
||||
}
|
||||
span := int64(maxLength-minLength) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding length: %w", err)
|
||||
}
|
||||
return minLength + int(offset.Int64()), nil
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Length and write-boundary templates come from controlled Minecraft 26.1.2
|
||||
// logins. Timing deliberately uses broad random bands that preserve only the
|
||||
// rough ordering of short and long phases; it does not replay captured delays.
|
||||
var startupPaddingSchedule2612 = []paddingTurn{
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2, 26, 16),
|
||||
},
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(26, 21, 25),
|
||||
},
|
||||
startDelay: millisecondRange(0, 20),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(25),
|
||||
},
|
||||
startDelay: millisecondRange(2, 22),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
registryPaddingVariant(),
|
||||
},
|
||||
startDelay: millisecondRange(20, 50),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2),
|
||||
},
|
||||
startDelay: millisecondRange(10, 35),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
playStartPaddingVariant(4941, 252, 259, 267, 268, 251, 303, 259, 264, 54, 346),
|
||||
playStartPaddingVariant(4941, 262, 284, 272, 260, 260, 313, 264, 151, 224, 207, 215, 224, 390),
|
||||
playStartPaddingVariant(4941, 257, 272, 275, 260, 260, 313, 283, 274, 226, 207, 230, 215, 204, 221, 352),
|
||||
playStartPaddingVariant(4941, 259, 272, 288, 260, 260, 311, 270, 70, 236, 223, 201, 210, 352),
|
||||
playStartPaddingVariant(4941, 255, 269, 277, 263, 260, 136, 207, 210, 232, 325),
|
||||
playStartPaddingVariant(4941, 259, 270, 274, 263, 258, 327, 170, 210, 375),
|
||||
playStartPaddingVariant(4941, 257, 275, 291, 260, 260, 325, 269, 70, 230, 226, 207, 221, 352),
|
||||
playStartPaddingVariant(4941, 252, 273, 262, 252, 254, 306, 93),
|
||||
playStartPaddingVariant(4941, 273, 270, 269, 258, 256, 322, 221, 207, 215, 438),
|
||||
playStartPaddingVariant(4941, 259, 275, 274, 250, 258, 308, 267, 154, 233, 209, 207, 213, 393),
|
||||
playStartPaddingVariant(4941, 254, 267, 272, 260, 253, 311, 167, 204, 232, 207, 481, 8),
|
||||
playStartPaddingVariant(4941, 259, 269, 272, 261, 313, 207, 213, 500, 19),
|
||||
playStartPaddingVariant(4941, 262, 269, 274, 263, 274, 311, 270, 242, 210, 229, 221, 210, 431),
|
||||
playStartPaddingVariant(4941, 259, 265, 277, 263, 277, 316, 269, 156, 204, 210, 226, 207, 413),
|
||||
playStartPaddingVariant(4941, 215, 251, 249, 317, 260, 270, 249, 52),
|
||||
playStartPaddingVariant(4941, 224, 263, 277, 316, 267, 272, 260, 138, 230, 226, 207, 204, 352),
|
||||
playStartPaddingVariant(4941, 221, 258, 263, 319, 269, 288, 263, 136, 204, 210, 220, 207, 378),
|
||||
playStartPaddingVariant(4941, 221, 258, 260, 316, 273, 291, 226, 204, 229, 213, 489, 8),
|
||||
playStartPaddingVariant(4941, 238, 260, 261, 306, 272, 277, 260, 224, 241, 212, 207, 204, 393),
|
||||
playStartPaddingVariant(4941, 224, 260, 260, 309, 272, 277, 277, 138, 207, 207, 212, 241, 352),
|
||||
},
|
||||
startDelay: millisecondRange(35, 50),
|
||||
},
|
||||
}
|
||||
|
||||
// These turns cover the finite Play-state tail through the client's
|
||||
// player_loaded packet. Bounds are the observed per-turn minima and maxima
|
||||
// across 20 controlled 26.1.2 logins; payload bytes remain opaque padding.
|
||||
var playJoinPaddingSchedule2612 = []paddingTurn{
|
||||
clientPlayPaddingTurn(6, 883),
|
||||
serverPlayPaddingTurn(346, 58638),
|
||||
clientPlayPaddingTurn(6, 887),
|
||||
serverPlayPaddingTurn(388, 61077),
|
||||
clientPlayPaddingTurn(2, 50),
|
||||
serverPlayPaddingTurn(575, 65584),
|
||||
clientPlayPaddingTurn(6, 45),
|
||||
serverPlayPaddingTurn(86, 63563),
|
||||
clientPlayPaddingTurn(2, 44),
|
||||
serverPlayPaddingTurn(42, 51983),
|
||||
clientPlayPaddingTurn(2, 851),
|
||||
serverPlayPaddingTurn(309, 25083),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(74, 63885),
|
||||
clientPlayPaddingTurn(8, 24),
|
||||
serverPlayPaddingTurn(30, 66128),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 35818),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(35, 59407),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(37, 65328),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 60622),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(11, 60808),
|
||||
clientPlayPaddingTurn(8, 43),
|
||||
serverPlayPaddingTurn(55, 62027),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(427, 65622),
|
||||
clientPlayPaddingTurn(5, 19),
|
||||
serverPlayPaddingTurn(35, 59401),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
}
|
||||
|
||||
type paddingLengthRange2612 struct {
|
||||
minimum int
|
||||
maximum int
|
||||
}
|
||||
|
||||
type serverPlayLengthBranches2612 struct {
|
||||
small paddingLengthRange2612
|
||||
large paddingLengthRange2612
|
||||
}
|
||||
|
||||
var serverPlayBranches2612 = []serverPlayLengthBranches2612{
|
||||
{small: paddingLengthRange2612{346, 18812}, large: paddingLengthRange2612{51702, 58638}},
|
||||
{small: paddingLengthRange2612{388, 20689}, large: paddingLengthRange2612{51445, 61077}},
|
||||
{small: paddingLengthRange2612{575, 20915}, large: paddingLengthRange2612{41428, 65584}},
|
||||
{small: paddingLengthRange2612{86, 2772}, large: paddingLengthRange2612{41428, 63563}},
|
||||
{small: paddingLengthRange2612{42, 26813}, large: paddingLengthRange2612{51983, 51983}},
|
||||
{small: paddingLengthRange2612{309, 19484}, large: paddingLengthRange2612{24837, 25083}},
|
||||
{small: paddingLengthRange2612{74, 40686}, large: paddingLengthRange2612{63885, 63885}},
|
||||
{small: paddingLengthRange2612{30, 44114}, large: paddingLengthRange2612{66128, 66128}},
|
||||
{small: paddingLengthRange2612{26, 1464}, large: paddingLengthRange2612{9941, 35818}},
|
||||
{small: paddingLengthRange2612{35, 42885}, large: paddingLengthRange2612{52194, 59407}},
|
||||
{small: paddingLengthRange2612{37, 47553}, large: paddingLengthRange2612{61765, 65328}},
|
||||
{small: paddingLengthRange2612{26, 1121}, large: paddingLengthRange2612{16162, 60622}},
|
||||
{small: paddingLengthRange2612{11, 45629}, large: paddingLengthRange2612{60808, 60808}},
|
||||
{small: paddingLengthRange2612{55, 10035}, large: paddingLengthRange2612{30237, 62027}},
|
||||
{small: paddingLengthRange2612{427, 52536}, large: paddingLengthRange2612{64014, 65622}},
|
||||
{small: paddingLengthRange2612{35, 22708}, large: paddingLengthRange2612{38987, 59401}},
|
||||
}
|
||||
|
||||
// Each mask preserves only the small/large branch order from one baseline
|
||||
// login. Actual lengths and timing are selected randomly inside each branch.
|
||||
var serverPlayBranchMasks2612 = []uint32{
|
||||
0x011c, 0x090a, 0x0821, 0xe921, 0x2102,
|
||||
0x0844, 0xa101, 0x1106, 0x2e00, 0xab01,
|
||||
0xe900, 0xac01, 0xab01, 0x8b80, 0x0808,
|
||||
0x2001, 0x0901, 0x000a, 0x2c01, 0x0801,
|
||||
}
|
||||
|
||||
type clientPlayBurst2612 struct {
|
||||
playIndex int
|
||||
regular paddingLengthRange2612
|
||||
burst paddingLengthRange2612
|
||||
}
|
||||
|
||||
var clientPlayBursts2612 = []clientPlayBurst2612{
|
||||
{playIndex: 0, regular: paddingLengthRange2612{6, 44}, burst: paddingLengthRange2612{877, 883}},
|
||||
{playIndex: 2, regular: paddingLengthRange2612{6, 45}, burst: paddingLengthRange2612{884, 887}},
|
||||
{playIndex: 10, regular: paddingLengthRange2612{2, 19}, burst: paddingLengthRange2612{851, 851}},
|
||||
}
|
||||
|
||||
// The 20 samples placed the one client initialization burst in these slots.
|
||||
var clientPlayBurstChoices2612 = []int{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1,
|
||||
2,
|
||||
}
|
||||
|
||||
var paddingSchedule2612 = buildPaddingSchedule2612()
|
||||
|
||||
func buildPaddingSchedule2612() []paddingTurn {
|
||||
schedule := make([]paddingTurn, 0, len(startupPaddingSchedule2612)+len(playJoinPaddingSchedule2612))
|
||||
schedule = append(schedule, startupPaddingSchedule2612...)
|
||||
schedule = append(schedule, playJoinPaddingSchedule2612...)
|
||||
return schedule
|
||||
}
|
||||
|
||||
func clientPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 30),
|
||||
writeChunkLength: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
func serverPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 45),
|
||||
chunkDelay: millisecondRange(1, 4),
|
||||
writeChunkMinLength: 32 * 1024,
|
||||
writeChunkLength: maxPaddingChunkLength,
|
||||
}
|
||||
}
|
||||
|
||||
func newClientPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
choice, err := randomPaddingIndex(len(clientPlayBurstChoices2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedBurst := clientPlayBurstChoices2612[choice]
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
for i, burst := range clientPlayBursts2612 {
|
||||
lengthRange := burst.regular
|
||||
if i == selectedBurst {
|
||||
lengthRange = burst.burst
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
type paddingPause struct {
|
||||
chunk int
|
||||
delay paddingDelayRange
|
||||
}
|
||||
|
||||
func paddingVariantFromChunks(chunks ...int) paddingVariant {
|
||||
return paddingVariant{chunks: chunks}
|
||||
}
|
||||
|
||||
func pacedPaddingVariant(chunks []int, pauses ...paddingPause) paddingVariant {
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
for _, pause := range pauses {
|
||||
if pause.chunk < 0 || pause.chunk >= len(delays) {
|
||||
panic("xmc: padding pause index is outside its chunk template")
|
||||
}
|
||||
delays[pause.chunk] = pause.delay
|
||||
}
|
||||
return paddingVariant{chunks: chunks, delays: delays}
|
||||
}
|
||||
|
||||
func registryPaddingVariant() paddingVariant {
|
||||
return pacedPaddingVariant(
|
||||
[]int{1590, 226, 329, 229, 186, 151, 78, 81, 79, 235, 67, 67, 78, 71, 82, 74, 982, 117, 1118, 1038, 970, 400, 239, 49, 50, 95, 65, 104, 32320, 2},
|
||||
paddingPause{28, millisecondRange(1, 4)},
|
||||
paddingPause{29, millisecondRange(44, 61)},
|
||||
)
|
||||
}
|
||||
|
||||
func playStartPaddingVariant(chunks ...int) paddingVariant {
|
||||
if len(chunks) < 2 {
|
||||
panic("xmc: play start padding variant needs at least two chunks")
|
||||
}
|
||||
return pacedPaddingVariant(
|
||||
chunks,
|
||||
paddingPause{len(chunks) / 2, millisecondRange(1, 5)},
|
||||
paddingPause{len(chunks) - 1, millisecondRange(9, 20)},
|
||||
)
|
||||
}
|
||||
|
||||
func millisecondRange(minimum, maximum int) paddingDelayRange {
|
||||
return paddingDelayRange{
|
||||
min: time.Duration(minimum) * time.Millisecond,
|
||||
max: time.Duration(maximum) * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
func newServerPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
profileIndex, err := randomPaddingIndex(len(serverPlayBranchMasks2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile := serverPlayBranchMasks2612[profileIndex]
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
lengthRange := branches.small
|
||||
if profile&(1<<i) != 0 {
|
||||
lengthRange = branches.large
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
@@ -1,618 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPaddingTurnReachesFinalTargetLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 128, maxLength: 128}
|
||||
const prefixLength = 3
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := prefixLength + encoded.Len(); got != turn.minLength {
|
||||
t.Fatalf("total turn length = %d, want %d", got, turn.minLength)
|
||||
}
|
||||
encodedReader := bytes.NewReader(encoded.Bytes())
|
||||
var recordLength Varint
|
||||
if err := recordLength.readFrom(encodedReader); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := int(recordLength); got != encoded.Len() {
|
||||
t.Fatalf("record length = %d, encoded = %d", got, encoded.Len())
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnSupportsThreeByteTarget(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := encoded.Len(); got != 3 {
|
||||
t.Fatalf("padding length = %d, want 3", got)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnVarintBoundaries(t *testing.T) {
|
||||
for _, targetLength := range []int{127, 128, 16383, 16384} {
|
||||
t.Run(strconv.Itoa(targetLength), func(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: targetLength, maxLength: targetLength}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() != targetLength {
|
||||
t.Fatalf("padding length = %d, want %d", encoded.Len(), targetLength)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnRandomRange(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingServerToClient, minLength: 127, maxLength: 129}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.minLength || encoded.Len() > turn.maxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnUsesRestrictedSendRange(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 3,
|
||||
maxLength: 100,
|
||||
sendMinLength: 90,
|
||||
sendMaxLength: 100,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.sendMinLength || encoded.Len() > turn.sendMaxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("restricted send range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingScheduleSynchronizesDirections(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
schedule := []paddingTurn{
|
||||
{direction: paddingClientToServer, minLength: 33, maxLength: 33},
|
||||
{direction: paddingServerToClient, minLength: 4097, maxLength: 4097},
|
||||
{direction: paddingClientToServer, minLength: 16385, maxLength: 16385},
|
||||
}
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
serverDone <- runPaddingSchedule(server, server, false, 3, schedule)
|
||||
}()
|
||||
if err := runPaddingSchedule(client, client, true, 3, schedule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("server padding schedule did not complete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHandlesFragmentedInput(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 1024, maxLength: 1024}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := readPaddingTurn(&oneByteReader{reader: bytes.NewReader(encoded.Bytes())}, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsInvalidLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 96}
|
||||
data := encodePaddingLength(t, 63)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "outside") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsNonCanonicalHeader(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
err := readPaddingTurn(bytes.NewReader([]byte{0x83, 0x00, 0x00}), turn, 0)
|
||||
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsTruncatedBody(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
data := encodePaddingLength(t, 64)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "body") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHonorsConnectionTimeout(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
err := readPaddingTurn(server, turn, 0)
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePaddingSchedule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule []paddingTurn
|
||||
prefix int
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "bad direction", schedule: []paddingTurn{{direction: 99, minLength: 4, maxLength: 4}}},
|
||||
{name: "too small", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 0, maxLength: 4}}},
|
||||
{name: "reversed range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 7}}},
|
||||
{name: "wrong first direction", prefix: 3, schedule: []paddingTurn{{direction: paddingServerToClient, minLength: 8, maxLength: 8}}},
|
||||
{name: "prefix leaves no header", prefix: 8, schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "same direction", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}, {direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "range with variants", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, variants: []paddingVariant{paddingVariantFromChunks(8)}}}},
|
||||
{name: "empty variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{}}}}},
|
||||
{name: "bad chunk", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(maxPaddingChunkLength + 1)}}}},
|
||||
{
|
||||
name: "delay mismatch",
|
||||
schedule: []paddingTurn{{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{4, 4},
|
||||
delays: []paddingDelayRange{{min: time.Millisecond, max: time.Millisecond}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
{name: "reversed start delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, startDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "reversed generated chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "oversized generated chunk", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkLength: maxPaddingChunkLength + 1}}},
|
||||
{name: "reversed generated chunk range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkMinLength: 9, writeChunkLength: 8}}},
|
||||
{name: "variant with generated chunks", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, writeChunkLength: 8}}},
|
||||
{name: "send range outside accepted range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 16, sendMinLength: 7, sendMaxLength: 12}}},
|
||||
{name: "variant with send range", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendMinLength: 8, sendMaxLength: 8}}},
|
||||
{name: "negative chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{chunks: []int{8}, delays: []paddingDelayRange{{min: -time.Millisecond}}}}}}},
|
||||
{name: "bad send variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendVariants: []int{1}}}},
|
||||
{name: "prefix splits chunk", prefix: 3, schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8, 4)}}}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := validatePaddingSchedule(test.schedule, test.prefix); err == nil {
|
||||
t.Fatal("expected invalid padding schedule")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612MatchesCapturedTemplates(t *testing.T) {
|
||||
wantDirections := []paddingDirection{
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
}
|
||||
wantLengths := [][]int{
|
||||
{44},
|
||||
{72},
|
||||
{25},
|
||||
{41172},
|
||||
{2},
|
||||
{7464, 8267, 8790, 8153, 7375, 7347, 8184, 6633, 7670, 8241, 7857, 7254, 8407, 8283, 6804, 8177, 8177, 7929, 8296, 8177},
|
||||
}
|
||||
if len(paddingSchedule2612) != len(wantDirections)+33 {
|
||||
t.Fatalf("padding schedule has %d turns, want %d", len(paddingSchedule2612), len(wantDirections)+33)
|
||||
}
|
||||
for i, turn := range paddingSchedule2612[:len(wantDirections)] {
|
||||
if turn.direction != wantDirections[i] {
|
||||
t.Fatalf("padding turn %d direction = %d, want %d", i, turn.direction, wantDirections[i])
|
||||
}
|
||||
if len(turn.variants) != len(wantLengths[i]) {
|
||||
t.Fatalf("padding turn %d has %d variants, want %d", i, len(turn.variants), len(wantLengths[i]))
|
||||
}
|
||||
for j, variant := range turn.variants {
|
||||
if got := paddingVariantLength(variant); got != wantLengths[i][j] {
|
||||
t.Fatalf("padding turn %d variant %d length = %d, want %d", i, j, got, wantLengths[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
wantPlayBounds := [][2]int{
|
||||
{6, 883},
|
||||
{346, 58638},
|
||||
{6, 887},
|
||||
{388, 61077},
|
||||
{2, 50},
|
||||
{575, 65584},
|
||||
{6, 45},
|
||||
{86, 63563},
|
||||
{2, 44},
|
||||
{42, 51983},
|
||||
{2, 851},
|
||||
{309, 25083},
|
||||
{2, 19},
|
||||
{74, 63885},
|
||||
{8, 24},
|
||||
{30, 66128},
|
||||
{2, 19},
|
||||
{26, 35818},
|
||||
{6, 19},
|
||||
{35, 59407},
|
||||
{6, 19},
|
||||
{37, 65328},
|
||||
{2, 19},
|
||||
{26, 60622},
|
||||
{6, 19},
|
||||
{11, 60808},
|
||||
{8, 43},
|
||||
{55, 62027},
|
||||
{2, 19},
|
||||
{427, 65622},
|
||||
{5, 19},
|
||||
{35, 59401},
|
||||
{6, 19},
|
||||
}
|
||||
for i, want := range wantPlayBounds {
|
||||
turn := paddingSchedule2612[len(wantDirections)+i]
|
||||
wantDirection := paddingClientToServer
|
||||
if i%2 == 1 {
|
||||
wantDirection = paddingServerToClient
|
||||
}
|
||||
if turn.direction != wantDirection {
|
||||
t.Fatalf("play turn %d direction = %d, want %d", i, turn.direction, wantDirection)
|
||||
}
|
||||
if turn.minLength != want[0] || turn.maxLength != want[1] {
|
||||
t.Fatalf("play turn %d bounds = %d-%d, want %d-%d", i, turn.minLength, turn.maxLength, want[0], want[1])
|
||||
}
|
||||
if len(turn.variants) != 0 {
|
||||
t.Fatalf("play turn %d unexpectedly has captured variants", i)
|
||||
}
|
||||
}
|
||||
if got := len(paddingSchedule2612[3].variants[0].chunks); got != 30 {
|
||||
t.Fatalf("registry turn chunks = %d, want 30", got)
|
||||
}
|
||||
minimumPlayStart := maxPaddingTurnLength
|
||||
maximumPlayStart := 0
|
||||
for _, variant := range paddingSchedule2612[5].variants {
|
||||
length := paddingVariantLength(variant)
|
||||
minimumPlayStart = min(minimumPlayStart, length)
|
||||
maximumPlayStart = max(maximumPlayStart, length)
|
||||
if variant.chunks[0] != 4941 {
|
||||
t.Fatalf("play start first chunk = %d, want 4941", variant.chunks[0])
|
||||
}
|
||||
}
|
||||
if minimumPlayStart != 6633 || maximumPlayStart != 8790 {
|
||||
t.Fatalf("play start bounds = %d-%d, want 6633-8790", minimumPlayStart, maximumPlayStart)
|
||||
}
|
||||
if err := validatePaddingSchedule(paddingSchedule2612, 2); err != nil {
|
||||
t.Fatalf("captured schedule is invalid: %v", err)
|
||||
}
|
||||
|
||||
serverSchedule, err := newServerPaddingSchedule2612()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = validatePaddingSchedule(serverSchedule, 2); err != nil {
|
||||
t.Fatalf("server schedule is invalid: %v", err)
|
||||
}
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
turn := serverSchedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
if got != branches.small && got != branches.large {
|
||||
t.Fatalf("server play turn %d send range = %v, want %v or %v", i, got, branches.small, branches.large)
|
||||
}
|
||||
}
|
||||
|
||||
for range 20 {
|
||||
clientSchedule, clientErr := newClientPaddingSchedule2612()
|
||||
if clientErr != nil {
|
||||
t.Fatal(clientErr)
|
||||
}
|
||||
if clientErr = validatePaddingSchedule(clientSchedule, 2); clientErr != nil {
|
||||
t.Fatalf("client schedule is invalid: %v", clientErr)
|
||||
}
|
||||
burstCount := 0
|
||||
for _, burst := range clientPlayBursts2612 {
|
||||
turn := clientSchedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
switch got {
|
||||
case burst.regular:
|
||||
case burst.burst:
|
||||
burstCount++
|
||||
default:
|
||||
t.Fatalf("client play turn %d send range = %v", burst.playIndex, got)
|
||||
}
|
||||
}
|
||||
if burstCount != 1 {
|
||||
t.Fatalf("client schedule has %d initialization bursts, want 1", burstCount)
|
||||
}
|
||||
}
|
||||
|
||||
for variantIndex := range paddingSchedule2612[3].variants {
|
||||
turn := paddingSchedule2612[3]
|
||||
turn.sendVariants = []int{variantIndex}
|
||||
var encoded bytes.Buffer
|
||||
if err = writePaddingTurnWithSleep(&encoded, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = readPaddingTurn(bytes.NewReader(encoded.Bytes()), paddingSchedule2612[3], 0); err != nil {
|
||||
t.Fatalf("registry variant %d was rejected: %v", variantIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantPreservesWriteBoundaries(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
turn paddingTurn
|
||||
prefix int
|
||||
want []int
|
||||
}{
|
||||
{name: "login acknowledged turn", turn: paddingSchedule2612[0], prefix: 2, want: []int{26, 16}},
|
||||
{name: "server response turn", turn: paddingSchedule2612[1], want: []int{26, 21, 25}},
|
||||
{name: "single packet turn", turn: paddingSchedule2612[2], want: []int{25}},
|
||||
{name: "fixed registry profile", turn: paddingSchedule2612[3], want: paddingSchedule2612[3].variants[0].chunks},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, test.turn, test.prefix, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(writer.writes) != len(test.want) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
for i := range test.want {
|
||||
if writer.writes[i] != test.want[i] {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), test.turn, test.prefix); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantAppliesPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
startDelay: paddingDelayRange{min: 3 * time.Millisecond, max: 3 * time.Millisecond},
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{3, 5, 7},
|
||||
delays: []paddingDelayRange{
|
||||
{},
|
||||
{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
{min: 4 * time.Millisecond, max: 4 * time.Millisecond},
|
||||
},
|
||||
}},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 3, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []time.Duration{3 * time.Millisecond, 2 * time.Millisecond, 4 * time.Millisecond}
|
||||
if len(slept) != len(want) {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
for i := range want {
|
||||
if slept[i] != want[i] {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunksApplyPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkLength: 32,
|
||||
chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantWrites := []int{32, 32, 32, 4}
|
||||
if !slicesEqual(writer.writes, wantWrites) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, wantWrites)
|
||||
}
|
||||
wantSleeps := []time.Duration{2 * time.Millisecond, 2 * time.Millisecond, 2 * time.Millisecond}
|
||||
if !slicesEqual(slept, wantSleeps) {
|
||||
t.Fatalf("delays = %v, want %v", slept, wantSleeps)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunkLengthIsRandomized(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkMinLength: 16,
|
||||
writeChunkLength: 32,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
firstWrite := writer.writes[0]
|
||||
if firstWrite < turn.writeChunkMinLength || firstWrite > turn.writeChunkLength {
|
||||
t.Fatalf("first write = %d", firstWrite)
|
||||
}
|
||||
seen[firstWrite] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("generated write chunk length did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingDelayRangeIsRandomized(t *testing.T) {
|
||||
delayRange := millisecondRange(25, 40)
|
||||
seen := make(map[time.Duration]bool)
|
||||
for range 100 {
|
||||
delay, err := randomPaddingDelay(delayRange)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if delay < delayRange.min || delay > delayRange.max {
|
||||
t.Fatalf("delay = %s, want %s-%s", delay, delayRange.min, delayRange.max)
|
||||
}
|
||||
seen[delay] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding delay did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612UsesCoarseTimingBands(t *testing.T) {
|
||||
assertDelayRange(t, "turn 3 to 4", paddingSchedule2612[3].startDelay, 20*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "turn 5 to 6", paddingSchedule2612[5].startDelay, 35*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "first play client turn", paddingSchedule2612[6].startDelay, time.Millisecond, 30*time.Millisecond)
|
||||
assertDelayRange(t, "first play server turn", paddingSchedule2612[7].startDelay, time.Millisecond, 45*time.Millisecond)
|
||||
assertDelayRange(t, "play server chunk pacing", paddingSchedule2612[7].chunkDelay, time.Millisecond, 4*time.Millisecond)
|
||||
if paddingSchedule2612[6].writeChunkLength != 1024 {
|
||||
t.Fatalf("play client write chunk = %d, want 1024", paddingSchedule2612[6].writeChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkLength != maxPaddingChunkLength {
|
||||
t.Fatalf("play server write chunk = %d, want %d", paddingSchedule2612[7].writeChunkLength, maxPaddingChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkMinLength != 32*1024 {
|
||||
t.Fatalf("play server minimum write chunk = %d, want %d", paddingSchedule2612[7].writeChunkMinLength, 32*1024)
|
||||
}
|
||||
|
||||
for i, variant := range paddingSchedule2612[3].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 45*time.Millisecond || maximum != 65*time.Millisecond {
|
||||
t.Fatalf("turn 4 variant %d duration = %s-%s, want 45ms-65ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
for i, variant := range paddingSchedule2612[5].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 10*time.Millisecond || maximum != 25*time.Millisecond {
|
||||
t.Fatalf("turn 6 variant %d duration = %s-%s, want 10ms-25ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertDelayRange(t *testing.T, name string, got paddingDelayRange, minimum, maximum time.Duration) {
|
||||
t.Helper()
|
||||
if got.min != minimum || got.max != maximum {
|
||||
t.Fatalf("%s delay = %s-%s, want %s-%s", name, got.min, got.max, minimum, maximum)
|
||||
}
|
||||
}
|
||||
|
||||
func paddingVariantDelayBounds(variant paddingVariant) (time.Duration, time.Duration) {
|
||||
var minimum time.Duration
|
||||
var maximum time.Duration
|
||||
for _, delay := range variant.delays {
|
||||
minimum += delay.min
|
||||
maximum += delay.max
|
||||
}
|
||||
return minimum, maximum
|
||||
}
|
||||
|
||||
func slicesEqual[T comparable](left, right []T) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i := range left {
|
||||
if left[i] != right[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func encodePaddingLength(t *testing.T, length int) []byte {
|
||||
t.Helper()
|
||||
var encoded bytes.Buffer
|
||||
value := Varint(length)
|
||||
if err := value.writeTo(&encoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return encoded.Bytes()
|
||||
}
|
||||
|
||||
type oneByteReader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (r *oneByteReader) Read(p []byte) (int, error) {
|
||||
if len(p) > 1 {
|
||||
p = p[:1]
|
||||
}
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
|
||||
type recordingWriter struct {
|
||||
bytes.Buffer
|
||||
writes []int
|
||||
}
|
||||
|
||||
func (w *recordingWriter) Write(p []byte) (int, error) {
|
||||
w.writes = append(w.writes, len(p))
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import "fmt"
|
||||
|
||||
type loginProfile struct {
|
||||
Username string
|
||||
UUID UUID
|
||||
TexturesValue string
|
||||
TexturesSignature string
|
||||
}
|
||||
|
||||
func profilesFromConfig(configured []*Profile) ([]loginProfile, error) {
|
||||
if len(configured) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
|
||||
profiles := make([]loginProfile, 0, len(configured))
|
||||
for _, configuredProfile := range configured {
|
||||
if configuredProfile == nil || configuredProfile.Username == "" {
|
||||
return nil, fmt.Errorf("invalid profile")
|
||||
}
|
||||
if len(configuredProfile.Uuid) != len(UUID{}) {
|
||||
return nil, fmt.Errorf("bad profile UUID length: %d", len(configuredProfile.Uuid))
|
||||
}
|
||||
if configuredProfile.TexturesValue == "" || configuredProfile.TexturesSignature == "" {
|
||||
return nil, fmt.Errorf("incomplete profile textures")
|
||||
}
|
||||
|
||||
profile := loginProfile{
|
||||
Username: configuredProfile.Username,
|
||||
TexturesValue: configuredProfile.TexturesValue,
|
||||
TexturesSignature: configuredProfile.TexturesSignature,
|
||||
}
|
||||
copy(profile.UUID[:], configuredProfile.Uuid)
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func findProfile(profiles []loginProfile, username string, uuid UUID) (loginProfile, bool) {
|
||||
for _, profile := range profiles {
|
||||
if profile.Username == username && profile.UUID == uuid {
|
||||
return profile, true
|
||||
}
|
||||
}
|
||||
return loginProfile{}, false
|
||||
}
|
||||
|
||||
func readLoginSuccess(packet *mcPacket) (loginProfile, error) {
|
||||
var (
|
||||
profile loginProfile
|
||||
username String
|
||||
propertyCount Varint
|
||||
propertyName String
|
||||
value String
|
||||
signed Boolean
|
||||
signature String
|
||||
)
|
||||
if err := packet.readFields(&profile.UUID, &username, &propertyCount, &propertyName, &value, &signed, &signature); err != nil {
|
||||
return loginProfile{}, err
|
||||
}
|
||||
if propertyCount != 1 || propertyName != "textures" || !signed {
|
||||
return loginProfile{}, fmt.Errorf("invalid login profile properties")
|
||||
}
|
||||
profile.Username = string(username)
|
||||
profile.TexturesValue = string(value)
|
||||
profile.TexturesSignature = string(signature)
|
||||
return profile, nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfilesFromConfigRejectsEmpty(t *testing.T) {
|
||||
if _, err := profilesFromConfig(nil); err == nil {
|
||||
t.Fatal("expected empty profiles error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilesFromConfig(t *testing.T) {
|
||||
uuid := bytes.Repeat([]byte{0x2a}, 16)
|
||||
profiles, err := profilesFromConfig([]*Profile{
|
||||
{
|
||||
Username: "SignedUser",
|
||||
Uuid: uuid,
|
||||
TexturesValue: "textures-value",
|
||||
TexturesSignature: "textures-signature",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build explicit profile: %v", err)
|
||||
}
|
||||
if len(profiles) != 1 || profiles[0].Username != "SignedUser" {
|
||||
t.Fatalf("unexpected profile: %+v", profiles)
|
||||
}
|
||||
if profiles[0].TexturesValue != "textures-value" || profiles[0].TexturesSignature != "textures-signature" {
|
||||
t.Fatalf("textures were not preserved: %+v", profiles[0])
|
||||
}
|
||||
}
|
||||
@@ -1,444 +0,0 @@
|
||||
// Minecraft protocol
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPacketDataLength = 32 * 1024
|
||||
maxPacketBodyLength = maxPacketDataLength + 5
|
||||
)
|
||||
|
||||
type field interface {
|
||||
readFrom(r io.Reader) error
|
||||
writeTo(w io.Writer) error
|
||||
}
|
||||
|
||||
type mcPacket struct {
|
||||
packetID int
|
||||
data []byte
|
||||
}
|
||||
|
||||
func readPacket(b io.Reader) (*mcPacket, error) {
|
||||
packet, _, err := readPacketWithLength(b)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func readPacketWithLength(b io.Reader) (*mcPacket, int, error) {
|
||||
packetData, wireLength, err := readFrame(b, maxPacketBodyLength)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
packet, err := decodePacketBody(packetData)
|
||||
return packet, wireLength, err
|
||||
}
|
||||
|
||||
func decodePacketBody(packetData []byte) (*mcPacket, error) {
|
||||
if len(packetData) < 1 || len(packetData) > maxPacketBodyLength {
|
||||
return nil, fmt.Errorf("read packet: bad length: %d", len(packetData))
|
||||
}
|
||||
|
||||
body := bytes.NewReader(packetData)
|
||||
var packetID Varint
|
||||
err := packetID.readFrom(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet ID: %w", err)
|
||||
}
|
||||
|
||||
dataLength := body.Len()
|
||||
if dataLength > maxPacketDataLength {
|
||||
return nil, fmt.Errorf("read packet: bad length: %d", dataLength)
|
||||
}
|
||||
|
||||
data := make([]byte, dataLength)
|
||||
_, err = io.ReadFull(body, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet data: %w", err)
|
||||
}
|
||||
|
||||
return &mcPacket{
|
||||
packetID: int(packetID),
|
||||
data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readFrame(r io.Reader, maxLength int) ([]byte, int, error) {
|
||||
frameLength, prefixLength, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("read packet length: %w", err)
|
||||
}
|
||||
if frameLength < 1 || int(frameLength) > maxLength {
|
||||
return nil, 0, fmt.Errorf("read packet: bad length: %d", frameLength)
|
||||
}
|
||||
|
||||
frame := make([]byte, int(frameLength))
|
||||
if _, err := io.ReadFull(r, frame); err != nil {
|
||||
return nil, 0, fmt.Errorf("read packet data: %w", err)
|
||||
}
|
||||
return frame, prefixLength + len(frame), nil
|
||||
}
|
||||
|
||||
func (p *mcPacket) readFields(fields ...field) error {
|
||||
r := bytes.NewReader(p.data)
|
||||
for _, field := range fields {
|
||||
err := field.readFrom(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read packet field: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Varint int32
|
||||
|
||||
const (
|
||||
SEGMENT_BITS = 0x7F
|
||||
CONTINUE_BIT = 0x80
|
||||
)
|
||||
|
||||
func (v *Varint) readFrom(r io.Reader) error {
|
||||
value, _, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func readVarintWithLength(r io.Reader) (Varint, int, error) {
|
||||
var value int32
|
||||
for index := 0; index < 5; index++ {
|
||||
currentByte, err := readByte(r)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("read varint: %w", err)
|
||||
}
|
||||
if index == 4 && currentByte&0xf0 != 0 {
|
||||
return 0, 0, fmt.Errorf("read varint: too large")
|
||||
}
|
||||
value |= int32(currentByte&SEGMENT_BITS) << (7 * index)
|
||||
|
||||
if currentByte&CONTINUE_BIT == 0 {
|
||||
parsed := Varint(value)
|
||||
length := index + 1
|
||||
if length != varintSize(parsed) {
|
||||
return 0, 0, fmt.Errorf("read varint: non-canonical encoding")
|
||||
}
|
||||
return parsed, length, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("read varint: too large")
|
||||
}
|
||||
|
||||
func (v *Varint) writeTo(w io.Writer) error {
|
||||
value := uint32(*v)
|
||||
|
||||
for {
|
||||
currentByte := byte(value & SEGMENT_BITS)
|
||||
value >>= 7
|
||||
if value != 0 {
|
||||
currentByte |= CONTINUE_BIT
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte{currentByte})
|
||||
if err != nil {
|
||||
return fmt.Errorf("write varint: %w", err)
|
||||
}
|
||||
|
||||
if value == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func varintSize(value Varint) int {
|
||||
uintValue := uint32(value)
|
||||
size := 0
|
||||
for range 5 {
|
||||
size++
|
||||
uintValue >>= 7
|
||||
if uintValue == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
type String string
|
||||
|
||||
func (v *String) readFrom(r io.Reader) error {
|
||||
var length Varint = 0
|
||||
|
||||
err := length.readFrom(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read string: %w", err)
|
||||
}
|
||||
|
||||
if length < 0 || length > 4096 {
|
||||
return fmt.Errorf("read string: bad length: %d", length)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
_, err = io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read string: %w", err)
|
||||
}
|
||||
|
||||
*v = String(string(buf))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *String) writeTo(w io.Writer) error {
|
||||
strBytes := []byte(*v)
|
||||
length := Varint(len(strBytes))
|
||||
|
||||
err := length.writeTo(w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write string: %w", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(strBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write string: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UnsignedShort uint16
|
||||
|
||||
func (v *UnsignedShort) readFrom(r io.Reader) error {
|
||||
var buf [2]byte
|
||||
_, err := io.ReadFull(r, buf[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read unsigned short: %w", err)
|
||||
}
|
||||
|
||||
*v = UnsignedShort(buf[0])<<8 | UnsignedShort(buf[1])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *UnsignedShort) writeTo(w io.Writer) error {
|
||||
buf := []byte{byte(*v >> 8), byte(*v & 0xFF)}
|
||||
_, err := w.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write unsigned short: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Long int64
|
||||
|
||||
func (v *Long) readFrom(r io.Reader) error {
|
||||
var buf [8]byte
|
||||
_, err := io.ReadFull(r, buf[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read long: %w", err)
|
||||
}
|
||||
|
||||
*v = Long(buf[0])<<56 | Long(buf[1])<<48 | Long(buf[2])<<40 | Long(buf[3])<<32 |
|
||||
Long(buf[4])<<24 | Long(buf[5])<<16 | Long(buf[6])<<8 | Long(buf[7])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Long) writeTo(w io.Writer) error {
|
||||
buf := []byte{
|
||||
byte(*v >> 56), byte((*v >> 48) & 0xFF), byte((*v >> 40) & 0xFF), byte((*v >> 32) & 0xFF),
|
||||
byte((*v >> 24) & 0xFF), byte((*v >> 16) & 0xFF), byte((*v >> 8) & 0xFF), byte(*v & 0xFF),
|
||||
}
|
||||
|
||||
_, err := w.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write long: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UUID [16]byte
|
||||
|
||||
func (v *UUID) readFrom(r io.Reader) error {
|
||||
_, err := io.ReadFull(r, v[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read UUID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Boolean bool
|
||||
|
||||
func (v *Boolean) readFrom(r io.Reader) error {
|
||||
b, err := readByte(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read boolean: %w", err)
|
||||
}
|
||||
if b > 1 {
|
||||
return fmt.Errorf("read boolean: invalid value: %d", b)
|
||||
}
|
||||
*v = b == 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Boolean) writeTo(w io.Writer) error {
|
||||
value := byte(0)
|
||||
if *v {
|
||||
value = 1
|
||||
}
|
||||
if _, err := w.Write([]byte{value}); err != nil {
|
||||
return fmt.Errorf("write boolean: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *UUID) writeTo(w io.Writer) error {
|
||||
_, err := w.Write(v[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("write UUID: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Bytes []byte
|
||||
|
||||
func (v *Bytes) readFrom(r io.Reader) error {
|
||||
var length Varint
|
||||
err := length.readFrom(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read bytes: %w", err)
|
||||
}
|
||||
|
||||
if length < 0 || length >= 1024 {
|
||||
return fmt.Errorf("read bytes: invalid size: %d", length)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
|
||||
_, err = io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read bytes: %w", err)
|
||||
}
|
||||
|
||||
*v = append([]byte(*v), buf...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RestBytes []byte
|
||||
|
||||
func (v *RestBytes) readFrom(r io.Reader) error {
|
||||
buf, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read remaining bytes: %w", err)
|
||||
}
|
||||
*v = append((*v)[:0], buf...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *RestBytes) writeTo(w io.Writer) error {
|
||||
if _, err := w.Write(*v); err != nil {
|
||||
return fmt.Errorf("write remaining bytes: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Bytes) writeTo(w io.Writer) error {
|
||||
length := Varint(len(*v))
|
||||
err := length.writeTo(w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bytes length: %w", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(*v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bytes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readByte(r io.Reader) (byte, error) {
|
||||
var buf [1]byte
|
||||
_, err := io.ReadFull(r, buf[:])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read byte: %w", err)
|
||||
}
|
||||
|
||||
return buf[0], nil
|
||||
}
|
||||
|
||||
func writePacket(w io.Writer, packetID int, fields ...field) error {
|
||||
_, err := writePacketWithLength(w, packetID, fields...)
|
||||
return err
|
||||
}
|
||||
|
||||
func writePacketWithLength(w io.Writer, packetID int, fields ...field) (int, error) {
|
||||
frame, err := encodePacket(packetID, fields...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = writeFull(w, frame); err != nil {
|
||||
return 0, fmt.Errorf("write packet data: %w", err)
|
||||
}
|
||||
return len(frame), nil
|
||||
}
|
||||
|
||||
func encodePacket(packetID int, fields ...field) ([]byte, error) {
|
||||
var dataBuf bytes.Buffer
|
||||
|
||||
for _, field := range fields {
|
||||
err := field.writeTo(&dataBuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write packet field: %w", err)
|
||||
}
|
||||
}
|
||||
if dataBuf.Len() > maxPacketDataLength {
|
||||
return nil, fmt.Errorf("write packet: bad length: %d", dataBuf.Len())
|
||||
}
|
||||
|
||||
packetIDVarint := Varint(packetID)
|
||||
bodyLength := varintSize(packetIDVarint) + dataBuf.Len()
|
||||
if bodyLength > maxPacketBodyLength {
|
||||
return nil, fmt.Errorf("write packet: bad length: %d", bodyLength)
|
||||
}
|
||||
|
||||
var frame bytes.Buffer
|
||||
frame.Grow(varintSize(Varint(bodyLength)) + bodyLength)
|
||||
frameLength := Varint(bodyLength)
|
||||
if err := frameLength.writeTo(&frame); err != nil {
|
||||
return nil, fmt.Errorf("write packet length: %w", err)
|
||||
}
|
||||
if err := packetIDVarint.writeTo(&frame); err != nil {
|
||||
return nil, fmt.Errorf("write packet ID: %w", err)
|
||||
}
|
||||
frame.Write(dataBuf.Bytes())
|
||||
return frame.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeFull(w io.Writer, p []byte) error {
|
||||
for len(p) > 0 {
|
||||
n, err := w.Write(p)
|
||||
if n > 0 {
|
||||
p = p[n:]
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeDisconnectPacket(w io.Writer, reason string) error {
|
||||
return writePacket(w, 0x00, new(String(reason)))
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadPacketDoesNotConsumeFollowingPacket(t *testing.T) {
|
||||
data := []byte{0x01, 0x80, 0x01, 0x00}
|
||||
r := bytes.NewReader(data)
|
||||
if _, err := readPacket(r); err == nil {
|
||||
t.Fatal("expected truncated packet ID to fail")
|
||||
}
|
||||
pkt, err := readPacket(r)
|
||||
if err != nil {
|
||||
t.Fatalf("read following packet: %v", err)
|
||||
}
|
||||
if pkt.packetID != 0 {
|
||||
t.Fatalf("packet ID = %d", pkt.packetID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketWithLengthReportsWireBytes(t *testing.T) {
|
||||
var wire bytes.Buffer
|
||||
written, err := writePacketWithLength(&wire, 0x03)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if written != 2 || !bytes.Equal(wire.Bytes(), []byte{0x01, 0x03}) {
|
||||
t.Fatalf("wire = %x, length = %d", wire.Bytes(), written)
|
||||
}
|
||||
|
||||
packet, read, err := readPacketWithLength(bytes.NewReader(wire.Bytes()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if packet.packetID != 0x03 || read != written {
|
||||
t.Fatalf("packet ID = %d, read = %d, written = %d", packet.packetID, read, written)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPacketRejectsNonCanonicalLengthVarint(t *testing.T) {
|
||||
_, _, err := readPacketWithLength(bytes.NewReader([]byte{0x81, 0x00, 0x03}))
|
||||
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarintRejectsOversizedFifthByte(t *testing.T) {
|
||||
var value Varint
|
||||
err := value.readFrom(bytes.NewReader([]byte{0xff, 0xff, 0xff, 0xff, 0x1f}))
|
||||
if err == nil || !strings.Contains(err.Error(), "too large") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Response by vanilla 26.1.2 server.
|
||||
var statusResponse = `{"description":"A Minecraft Server","players":{"max":20,"online":0},"version":{"name":"26.1.2","protocol":775},"enforcesSecureChat":true}`
|
||||
|
||||
type serverState int
|
||||
|
||||
var (
|
||||
serverStateHandshake serverState = 1
|
||||
serverStateProxy serverState = 3
|
||||
)
|
||||
|
||||
type serverConn struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
c net.Conn
|
||||
|
||||
state serverState
|
||||
|
||||
handshakeLock sync.Mutex
|
||||
lifecycleMu sync.Mutex
|
||||
closed bool
|
||||
profiles []loginProfile
|
||||
password string
|
||||
rsaPrivateKey *rsa.PrivateKey
|
||||
rsaPublicKey []byte
|
||||
paddingSchedule []paddingTurn
|
||||
packet *packetStream
|
||||
deadlines *connectionDeadlines
|
||||
}
|
||||
|
||||
func (c *serverConn) handshake() error {
|
||||
c.handshakeLock.Lock()
|
||||
defer c.handshakeLock.Unlock()
|
||||
|
||||
if c.state != serverStateHandshake {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.deadlines.beginHandshake(); err != nil {
|
||||
return fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
defer func() { _ = c.deadlines.endHandshake() }()
|
||||
|
||||
var (
|
||||
protocolVersion Varint
|
||||
serverAddress String
|
||||
serverPort UnsignedShort
|
||||
nextState Varint
|
||||
)
|
||||
|
||||
// handshake packet
|
||||
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read handshake packet: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0 {
|
||||
return fmt.Errorf("bad handshake packet id")
|
||||
}
|
||||
|
||||
err = pkt.readFields(&protocolVersion, &serverAddress, &serverPort, &nextState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read handshake packet: %w", err)
|
||||
}
|
||||
|
||||
switch nextState {
|
||||
case 1:
|
||||
|
||||
// Ping
|
||||
|
||||
for range 2 {
|
||||
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read packet: %w", err)
|
||||
}
|
||||
|
||||
switch pkt.packetID {
|
||||
case 0: // Status Request
|
||||
|
||||
err = writePacket(c.writer, 0, new(String(statusResponse)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("write status response: %w", err)
|
||||
}
|
||||
|
||||
case 1: // Ping
|
||||
|
||||
var payload Long
|
||||
err = pkt.readFields(&payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read ping packet: %w", err)
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 1, &payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write ping response: %w", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return fmt.Errorf("ping")
|
||||
|
||||
case 2:
|
||||
|
||||
// Login
|
||||
|
||||
// login start
|
||||
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login start packet: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0 {
|
||||
return fmt.Errorf("bad login start packet id")
|
||||
}
|
||||
|
||||
var (
|
||||
username String
|
||||
uuid UUID
|
||||
)
|
||||
|
||||
err = pkt.readFields(&username, &uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login start packet: %w", err)
|
||||
}
|
||||
profile, found := findProfile(c.profiles, string(username), uuid)
|
||||
|
||||
// encrypt request
|
||||
|
||||
var (
|
||||
serverId String = String("")
|
||||
publicKey Bytes = Bytes(c.rsaPublicKey)
|
||||
verifyToken Bytes = Bytes(make([]byte, 4))
|
||||
shouldAuthenticate Boolean = true
|
||||
)
|
||||
|
||||
if _, err = rand.Read(verifyToken); err != nil {
|
||||
return fmt.Errorf("generate verify token: %w", err)
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 0x01, &serverId, &publicKey, &verifyToken, &shouldAuthenticate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write encryption request: %w", err)
|
||||
}
|
||||
|
||||
// encrypt response
|
||||
|
||||
var (
|
||||
encryptedSharedSecret Bytes
|
||||
encryptedVerifyToken Bytes
|
||||
|
||||
sharedSecret []byte
|
||||
decryptedVerifyToken []byte
|
||||
)
|
||||
|
||||
pkt, err = readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encrypt response: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0x01 {
|
||||
return fmt.Errorf("bad encrypt response packet id")
|
||||
}
|
||||
|
||||
err = pkt.readFields(&encryptedSharedSecret, &encryptedVerifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encrypt response: %w", err)
|
||||
}
|
||||
|
||||
sharedSecret, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedSharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt shared secret: %w", err)
|
||||
}
|
||||
if len(sharedSecret) != 16 {
|
||||
return fmt.Errorf("bad shared secret length: %d", len(sharedSecret))
|
||||
}
|
||||
|
||||
decryptedVerifyToken, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedVerifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt verify token: %w", err)
|
||||
}
|
||||
|
||||
if len(decryptedVerifyToken) < 4 || !bytes.Equal(verifyToken, decryptedVerifyToken[:4]) {
|
||||
return fmt.Errorf("verify token mismatch")
|
||||
}
|
||||
|
||||
c.reader, err = newCryptoReader(c.reader, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto reader: %w", err)
|
||||
}
|
||||
|
||||
c.writer, err = newCryptoWriter(c.writer, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto writer: %w", err)
|
||||
}
|
||||
|
||||
// verify password
|
||||
receivedPassword := decryptedVerifyToken[4:]
|
||||
|
||||
if subtle.ConstantTimeCompare(receivedPassword, []byte(c.password)) != 1 {
|
||||
writeDisconnectPacket(c.writer, `{"type":"translatable","translate":"multiplayer.disconnect.authservers_down"}`)
|
||||
return fmt.Errorf("bad password")
|
||||
}
|
||||
if !found {
|
||||
if err = writeDisconnectPacket(c.writer, `{"text":"You are not white-listed on this server!"}`); err != nil {
|
||||
return fmt.Errorf("write unknown login profile disconnect: %w", err)
|
||||
}
|
||||
return fmt.Errorf("unknown login profile")
|
||||
}
|
||||
|
||||
loginName := String(profile.Username)
|
||||
propertyCount := Varint(1)
|
||||
propertyName := String("textures")
|
||||
texturesValue := String(profile.TexturesValue)
|
||||
signed := Boolean(true)
|
||||
texturesSignature := String(profile.TexturesSignature)
|
||||
if err = writePacket(c.writer, 0x02, &profile.UUID, &loginName, &propertyCount, &propertyName, &texturesValue, &signed, &texturesSignature); err != nil {
|
||||
return fmt.Errorf("write login finished: %w", err)
|
||||
}
|
||||
|
||||
var loginAcknowledgedLength int
|
||||
pkt, loginAcknowledgedLength, err = readPacketWithLength(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login acknowledged: %w", err)
|
||||
}
|
||||
if err = validateLoginAcknowledgedPacket(pkt); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = runPaddingSchedule(c.reader, c.writer, false, loginAcknowledgedLength, c.paddingSchedule); err != nil {
|
||||
return fmt.Errorf("run startup padding: %w", err)
|
||||
}
|
||||
|
||||
packet := newPacketStream(c.reader, c.writer, false)
|
||||
c.lifecycleMu.Lock()
|
||||
if c.closed {
|
||||
c.lifecycleMu.Unlock()
|
||||
packet.Stop()
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.packet = packet
|
||||
c.reader = packet
|
||||
c.writer = packet
|
||||
c.state = serverStateProxy
|
||||
c.lifecycleMu.Unlock()
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("bad handshake packet: bad next state: %d", nextState)
|
||||
}
|
||||
}
|
||||
|
||||
func validateLoginAcknowledgedPacket(pkt *mcPacket) error {
|
||||
if pkt.packetID != 0x03 {
|
||||
return fmt.Errorf("bad login acknowledged packet id: %d", pkt.packetID)
|
||||
}
|
||||
if len(pkt.data) != 0 {
|
||||
return fmt.Errorf("bad login acknowledged packet data length: %d", len(pkt.data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConn) Read(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.reader.Read(b)
|
||||
}
|
||||
|
||||
func (c *serverConn) Write(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.writer.Write(b)
|
||||
}
|
||||
|
||||
func (c *serverConn) Close() error {
|
||||
c.lifecycleMu.Lock()
|
||||
c.closed = true
|
||||
packet := c.packet
|
||||
c.lifecycleMu.Unlock()
|
||||
if packet != nil {
|
||||
packet.Stop()
|
||||
}
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c *serverConn) LocalAddr() net.Addr {
|
||||
return c.c.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *serverConn) RemoteAddr() net.Addr {
|
||||
return c.c.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *serverConn) SetDeadline(t time.Time) error {
|
||||
return c.deadlines.setDeadline(t)
|
||||
}
|
||||
|
||||
func (c *serverConn) SetReadDeadline(t time.Time) error {
|
||||
return c.deadlines.setReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *serverConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.deadlines.setWriteDeadline(t)
|
||||
}
|
||||
|
||||
func wrapConnServer(c net.Conn, profiles []loginProfile, password string, rsaPrivateKeyDER []byte, rsaPublicKey []byte) (*serverConn, error) {
|
||||
if len(profiles) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
if len(rsaPrivateKeyDER) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa private key")
|
||||
}
|
||||
if len(rsaPublicKey) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa public key")
|
||||
}
|
||||
rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(rsaPrivateKeyDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse rsa private key: %w", err)
|
||||
}
|
||||
paddingSchedule, err := newServerPaddingSchedule2612()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select padding profile: %w", err)
|
||||
}
|
||||
|
||||
s := &serverConn{
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: serverStateHandshake,
|
||||
profiles: profiles,
|
||||
password: password,
|
||||
rsaPrivateKey: rsaPrivateKey,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
paddingSchedule: paddingSchedule,
|
||||
deadlines: newConnectionDeadlines(c),
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Minecraft stream cipher
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type cryptoStream struct {
|
||||
stream cipher.Stream
|
||||
r io.Reader
|
||||
w io.Writer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newCryptoReader(r io.Reader, sharedSecret []byte) (*cryptoStream, error) {
|
||||
blockCipher, err := aes.NewCipher(sharedSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new aes cipher: %w", err)
|
||||
}
|
||||
|
||||
stream := newCFB8Decrypt(blockCipher, sharedSecret)
|
||||
|
||||
return &cryptoStream{stream: stream, r: r}, nil
|
||||
}
|
||||
|
||||
func (c *cryptoStream) Read(b []byte) (int, error) {
|
||||
if c.r == nil {
|
||||
panic("read on a write-only crypto stream")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
n, err := c.r.Read(b)
|
||||
if n > 0 {
|
||||
c.stream.XORKeyStream(b[:n], b[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, fmt.Errorf("crypto reader: read: %w", err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func newCryptoWriter(w io.Writer, sharedSecret []byte) (*cryptoStream, error) {
|
||||
blockCipher, err := aes.NewCipher(sharedSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new aes cipher: %w", err)
|
||||
}
|
||||
|
||||
stream := newCFB8Encrypt(blockCipher, sharedSecret)
|
||||
|
||||
return &cryptoStream{stream: stream, w: w}, nil
|
||||
}
|
||||
|
||||
func (c *cryptoStream) Write(b []byte) (int, error) {
|
||||
if c.w == nil {
|
||||
panic("write on a read-only crypto stream")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
encrypted := make([]byte, len(b))
|
||||
c.stream.XORKeyStream(encrypted, b)
|
||||
|
||||
if err := writeFull(c.w, encrypted); err != nil {
|
||||
return 0, fmt.Errorf("crypto writer: write: %w", err)
|
||||
}
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type dataAndEOFReader struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (r *dataAndEOFReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, r.data)
|
||||
r.data = r.data[n:]
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
type shortWriter struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *shortWriter) Write(p []byte) (int, error) {
|
||||
if len(p) > 1 {
|
||||
p = p[:len(p)/2]
|
||||
}
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
|
||||
func TestCryptoReaderPreservesDataReturnedWithEOF(t *testing.T) {
|
||||
secret := []byte("0123456789abcdef")
|
||||
plaintext := []byte("payload returned with EOF")
|
||||
var encrypted bytes.Buffer
|
||||
writer, err := newCryptoWriter(&encrypted, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = writer.Write(plaintext); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reader, err := newCryptoReader(&dataAndEOFReader{data: encrypted.Bytes()}, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(plaintext))
|
||||
n, err := reader.Read(got)
|
||||
if err == nil || n != len(plaintext) {
|
||||
t.Fatalf("Read = %d, %v", n, err)
|
||||
}
|
||||
if !bytes.Equal(got[:n], plaintext) {
|
||||
t.Fatalf("plaintext = %q", got[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCryptoWriterHandlesShortWrites(t *testing.T) {
|
||||
secret := []byte("0123456789abcdef")
|
||||
plaintext := bytes.Repeat([]byte("short-write"), 100)
|
||||
var dst shortWriter
|
||||
writer, err := newCryptoWriter(&dst, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, err := writer.Write(plaintext); err != nil || n != len(plaintext) {
|
||||
t.Fatalf("Write = %d, %v", n, err)
|
||||
}
|
||||
reader, err := newCryptoReader(bytes.NewReader(dst.Bytes()), secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Fatal("decrypted payload mismatch")
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,10 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
config := tlsConfig.GetTLSConfig(tls.WithDestination(dest))
|
||||
config := tlsConfig.GetTLSConfig()
|
||||
if config.ServerName == "" && address.Family().IsDomain() {
|
||||
config.ServerName = address.Domain()
|
||||
}
|
||||
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
|
||||
return tls.UClient(c, config, fingerprint), nil
|
||||
} else { // Fallback to normal gRPC TLS
|
||||
|
||||
@@ -38,14 +38,12 @@ func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter
|
||||
|
||||
func NewHunkConn(hc HunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
|
||||
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
|
||||
lAddr := localAddrFromContext(hc.Context())
|
||||
wrc := NewHunkReadWriter(hc, cancel)
|
||||
return cnc.NewConnection(
|
||||
cnc.ConnectionInput(wrc),
|
||||
cnc.ConnectionOutput(wrc),
|
||||
cnc.ConnectionOnClose(wrc),
|
||||
cnc.ConnectionRemoteAddr(rAddr),
|
||||
cnc.ConnectionLocalAddr(lAddr),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,14 +33,12 @@ func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiH
|
||||
|
||||
func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
|
||||
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
|
||||
lAddr := localAddrFromContext(hc.Context())
|
||||
wrc := NewMultiHunkReadWriter(hc, cancel)
|
||||
return cnc.NewConnection(
|
||||
cnc.ConnectionInputMulti(wrc),
|
||||
cnc.ConnectionOutputMulti(wrc),
|
||||
cnc.ConnectionOnClose(wrc),
|
||||
cnc.ConnectionRemoteAddr(rAddr),
|
||||
cnc.ConnectionLocalAddr(lAddr),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,17 +56,3 @@ func parseTrustedXForwardedFor(md metadata.MD, trusted []string, remoteAddr net.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func localAddrFromContext(ctx context.Context) net.Addr {
|
||||
var localAddr net.Addr
|
||||
if pr, ok := peer.FromContext(ctx); ok {
|
||||
localAddr = pr.LocalAddr
|
||||
}
|
||||
if localAddr == nil {
|
||||
localAddr = &net.TCPAddr{
|
||||
IP: []byte{0, 0, 0, 0},
|
||||
Port: 0,
|
||||
}
|
||||
}
|
||||
return localAddr
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -45,11 +43,7 @@ func (s *server) Handle(conn net.Conn) {
|
||||
|
||||
// upgrade execute a fake websocket upgrade process and return the available connection
|
||||
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
|
||||
// timeout and header limit are the same as websocket
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second * 4))
|
||||
defer conn.SetReadDeadline(time.Time{})
|
||||
connReader := bufio.NewReader(io.LimitReader(conn, 12288))
|
||||
|
||||
connReader := bufio.NewReader(conn)
|
||||
req, err := http.ReadRequest(connReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -100,15 +94,7 @@ func (s *server) keepAccepting() {
|
||||
for {
|
||||
conn, err := s.innnerListener.Accept()
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "closed") {
|
||||
break
|
||||
}
|
||||
errors.LogWarningInner(context.Background(), err, "failed to accept raw connections")
|
||||
if strings.Contains(errStr, "too many") {
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
}
|
||||
continue
|
||||
return
|
||||
}
|
||||
go s.Handle(conn)
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
||||
c = &client{
|
||||
dest: dest,
|
||||
config: streamSettings.ProtocolSettings.(*Config),
|
||||
tlsConfig: tlsConfig.GetTLSConfig(tls.WithDestination(dest)),
|
||||
tlsConfig: tlsConfig.GetTLSConfig(),
|
||||
socketConfig: streamSettings.SocketSettings,
|
||||
udpmaskManager: streamSettings.UdpmaskManager,
|
||||
quicParams: streamSettings.QuicParams,
|
||||
|
||||
@@ -24,11 +24,8 @@ func (c *Config) GetNormalizedPath() string {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
if c.GetNormalizedSessionPlacement() == PlacementPath ||
|
||||
c.GetNormalizedSeqPlacement() == PlacementPath {
|
||||
if path[len(path)-1] != '/' {
|
||||
path = path + "/"
|
||||
}
|
||||
if path[len(path)-1] != '/' {
|
||||
path = path + "/"
|
||||
}
|
||||
|
||||
return path
|
||||
|
||||
@@ -3,77 +3,16 @@ package splithttp_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
. "github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
)
|
||||
|
||||
func Test_GetNormalizedPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
TestName string
|
||||
Path string
|
||||
SessionIDPlacement string
|
||||
SeqPlacement string
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
TestName: "default placement keeps trailing slash",
|
||||
Path: "/sh",
|
||||
Expected: "/sh/",
|
||||
},
|
||||
{
|
||||
TestName: "query string is stripped",
|
||||
Path: "/?world",
|
||||
Expected: "/",
|
||||
},
|
||||
{
|
||||
TestName: "both off path drops trailing slash",
|
||||
Path: "/stream",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "query",
|
||||
Expected: "/stream",
|
||||
},
|
||||
{
|
||||
TestName: "both off path keeps file-like path",
|
||||
Path: "/stream/filename.extension",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "header",
|
||||
Expected: "/stream/filename.extension",
|
||||
},
|
||||
{
|
||||
TestName: "seq in path keeps trailing slash",
|
||||
Path: "/stream",
|
||||
SessionIDPlacement: "query",
|
||||
Expected: "/stream/",
|
||||
},
|
||||
{
|
||||
TestName: "session in path keeps trailing slash",
|
||||
Path: "/stream",
|
||||
SeqPlacement: "cookie",
|
||||
Expected: "/stream/",
|
||||
},
|
||||
{
|
||||
TestName: "existing trailing slash preserved",
|
||||
Path: "/stream/",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "query",
|
||||
Expected: "/stream/",
|
||||
},
|
||||
{
|
||||
TestName: "root unchanged",
|
||||
Path: "/",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "query",
|
||||
Expected: "/",
|
||||
},
|
||||
c := Config{
|
||||
Path: "/?world",
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.TestName, func(t *testing.T) {
|
||||
c := Config{
|
||||
Path: test.Path,
|
||||
SessionIDPlacement: test.SessionIDPlacement,
|
||||
SeqPlacement: test.SeqPlacement,
|
||||
}
|
||||
assert.Equal(t, test.Expected, c.GetNormalizedPath())
|
||||
})
|
||||
|
||||
path := c.GetNormalizedPath()
|
||||
if path != "/" {
|
||||
t.Error("Unexpected: ", path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,15 +373,11 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
|
||||
Reader: request.Body,
|
||||
ResponseWriter: writer,
|
||||
}
|
||||
localAddr := h.localAddr
|
||||
if la, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && la != nil {
|
||||
localAddr = la
|
||||
}
|
||||
conn := splitConn{
|
||||
writer: httpSC,
|
||||
reader: httpSC,
|
||||
remoteAddr: remoteAddr,
|
||||
localAddr: localAddr,
|
||||
localAddr: h.localAddr,
|
||||
}
|
||||
if sessionId != "" { // if not stream-one
|
||||
conn.reader = currentSession.uploadQueue
|
||||
|
||||
@@ -331,9 +331,6 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509
|
||||
}
|
||||
|
||||
if verifyResult == foundCA { // if found CA, we need to verify here
|
||||
if len(r.Config.ServerName) == 0 {
|
||||
return errors.New("Pinning CA needs a valid ServerName")
|
||||
}
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: CAs,
|
||||
CurrentTime: time.Now(),
|
||||
@@ -453,13 +450,9 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
|
||||
for _, s := range tls.CipherSuites() {
|
||||
id[s.Name] = s.ID
|
||||
}
|
||||
for _, s := range tls.InsecureCipherSuites() {
|
||||
id[s.Name] = s.ID
|
||||
}
|
||||
for n := range strings.SplitSeq(c.CipherSuites, ":") {
|
||||
n = strings.TrimSpace(n)
|
||||
if v, ok := id[n]; ok {
|
||||
config.CipherSuites = append(config.CipherSuites, v)
|
||||
for _, n := range strings.Split(c.CipherSuites, ":") {
|
||||
if id[n] != 0 {
|
||||
config.CipherSuites = append(config.CipherSuites, id[n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,16 +55,19 @@ func ApplyECH(c *Config, config *tls.Config) error {
|
||||
}
|
||||
config.EncryptedClientHelloConfigList = ECHConfig
|
||||
}()
|
||||
// query config from dns
|
||||
// direct base64 config
|
||||
if strings.Contains(c.EchConfigList, "://") {
|
||||
// parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query"
|
||||
parts := strings.SplitN(c.EchConfigList, "+", 2)
|
||||
// query config from dns
|
||||
parts := strings.Split(c.EchConfigList, "+")
|
||||
if len(parts) == 2 {
|
||||
// parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query"
|
||||
nameToQuery = parts[0]
|
||||
DNSServer = parts[1]
|
||||
} else {
|
||||
} else if len(parts) == 1 {
|
||||
// normal format
|
||||
DNSServer = parts[0]
|
||||
} else {
|
||||
return errors.New("Invalid ECH DNS server format: ", c.EchConfigList)
|
||||
}
|
||||
if nameToQuery == "" {
|
||||
return errors.New("Using DNS for ECH Config needs serverName or use Server format example.com+https://1.1.1.1/dns-query")
|
||||
@@ -74,7 +77,6 @@ func ApplyECH(c *Config, config *tls.Config) error {
|
||||
return errors.New("Failed to query ECH DNS record for domain: ", nameToQuery, " at server: ", DNSServer).Base(err)
|
||||
}
|
||||
} else {
|
||||
// direct base64 config
|
||||
ECHConfig, err = base64.StdEncoding.DecodeString(c.EchConfigList)
|
||||
if err != nil {
|
||||
return errors.New("Failed to unmarshal ECHConfigList: ", err)
|
||||
@@ -155,7 +157,7 @@ func QueryRecord(domain string, server string, sockopt *internet.SocketConfig) (
|
||||
// If expire is zero value, it means we are in initial state, wait for the query to finish
|
||||
// otherwise return old value immediately and update in a goroutine
|
||||
// but if the cache is too old, wait for update
|
||||
if configRecord.expire.IsZero() || configRecord.expire.Add(time.Hour*4).Before(time.Now()) {
|
||||
if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*4).Before(time.Now()) {
|
||||
return echConfigCache.Update(domain, server, false, sockopt)
|
||||
} else {
|
||||
// If someone already acquired the lock, it means it is updating, do not start another update goroutine
|
||||
@@ -254,17 +256,14 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
|
||||
}
|
||||
dnsResolve = respBody
|
||||
} else if strings.HasPrefix(server, "udp://") { // for classic udp dns server
|
||||
udpServerURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, 0, errors.New("failed to parse udp dns server ", server, " for ECH: ", err)
|
||||
}
|
||||
udpServerAddr := server[len("udp://"):]
|
||||
// default port 53 if not specified
|
||||
if udpServerURL.Port() == "" {
|
||||
udpServerURL.Host = udpServerURL.Host + ":53"
|
||||
if !strings.Contains(udpServerAddr, ":") {
|
||||
udpServerAddr = udpServerAddr + ":53"
|
||||
}
|
||||
dest, err := net.ParseDestination("udp" + ":" + udpServerURL.Host)
|
||||
dest, err := net.ParseDestination("udp" + ":" + udpServerAddr)
|
||||
if err != nil {
|
||||
return nil, 0, errors.New("failed to parse udp dns server ", udpServerURL.Host, " for ECH: ", err)
|
||||
return nil, 0, errors.New("failed to parse udp dns server ", udpServerAddr, " for ECH: ", err)
|
||||
}
|
||||
dnsTimeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -19,7 +19,8 @@ func TestECHDial(t *testing.T) {
|
||||
// test concurrent Dial(to test cache problem)
|
||||
wg := sync.WaitGroup{}
|
||||
for range 10 {
|
||||
wg.Go(func() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
TLSConfig := config.GetTLSConfig()
|
||||
TLSConfig.NextProtos = []string{"http/1.1"}
|
||||
client := &http.Client{
|
||||
@@ -35,7 +36,8 @@ func TestECHDial(t *testing.T) {
|
||||
if !strings.Contains(string(body), "sni=encrypted") {
|
||||
t.Error("ECH Dial success but SNI is not encrypted")
|
||||
}
|
||||
})
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
// check cache
|
||||
|
||||
Reference in New Issue
Block a user