diff --git a/.github/linters/.golangci.yml b/.github/linters/.golangci.yml new file mode 100644 index 00000000..36504643 --- /dev/null +++ b/.github/linters/.golangci.yml @@ -0,0 +1,46 @@ +run: + modules-download-mode: vendor + skip-dirs: + - generated.* + - external + +linters: + enable: + - bodyclose + - deadcode + - depguard + - dogsled + - dupl + - errcheck + - exhaustive + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - golint + - gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - interfacer + - lll + - misspell + - nakedret + - noctx + - nolintlint + - rowserrcheck + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - whitespace diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml deleted file mode 100644 index 47326cb2..00000000 --- a/.github/workflows/docker.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Release Docker Image - -on: - push: - tags: - - v* - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Checkout default branch - uses: actions/checkout@v2 - - - name: Install Buildx and QEMU - run: | - export DOCKER_BUILDKIT=1 - docker build --platform=local -o . git://github.com/docker/buildx - mkdir -p ~/.docker/cli-plugins - mv buildx ~/.docker/cli-plugins/docker-buildx - docker run --rm --privileged multiarch/qemu-user-static:latest --reset -p yes --credential yes - docker buildx create --use --name build --node build --driver-opt network=host - - - name: Login to Docker Hub - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: | - echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - - name: Build and push Docker image - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_IMAGE_PLATFORM: linux/386,linux/amd64,linux/arm/v7,linux/arm64 - run: | - DOCKER_IMAGE_NAME=$(echo $DOCKER_USERNAME | tr '[:upper:]' '[:lower:]')/v2fly-core - DOCKER_IMAGE_VERSION=${GITHUB_REF#refs/*/} - docker buildx build \ - --platform "$DOCKER_IMAGE_PLATFORM" \ - --output "type=image,push=true" \ - --tag "$DOCKER_IMAGE_NAME":"$DOCKER_IMAGE_VERSION" \ - --tag "$DOCKER_IMAGE_NAME":latest \ - --file ./Dockerfile . diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 0204ecfb..8b1ed9e5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -26,16 +26,6 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go- - - name: Show if need to format code - if: ${{ always() }} - run: | - filesNeedToFormat=$(go fmt ./...) - if [[ $filesNeedToFormat ]]; then - echo -e "\033[0;36m[Error] The following Go files need to be formatted:\033[0m" - echo -e "\033[0;31m$filesNeedToFormat\033[0m" - exit 1 - fi - - name: Lint *.go files if: ${{ always() }} run: | @@ -46,7 +36,7 @@ jobs: - name: Lint other files if: ${{ always() }} - uses: github/super-linter@v3.9.2 + uses: github/super-linter@v3.9.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false @@ -57,3 +47,13 @@ jobs: VALIDATE_JSON: false VALIDATE_MD: false VALIDATE_PROTOBUF: false + + - name: Show if need to format code + if: ${{ always() }} + run: | + filesNeedToFormat=$(go fmt ./...) + if [[ $filesNeedToFormat ]]; then + echo -e "\033[0;36m[Error] The following Go files need to be formatted:\033[0m" + echo -e "\033[0;31m$filesNeedToFormat\033[0m" + exit 1 + fi diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index 7c22e05a..c5aaf84f 100644 --- a/app/dispatcher/default.go +++ b/app/dispatcher/default.go @@ -20,6 +20,7 @@ import ( "v2ray.com/core/features/outbound" "v2ray.com/core/features/policy" "v2ray.com/core/features/routing" + routing_session "v2ray.com/core/features/routing/session" "v2ray.com/core/features/stats" "v2ray.com/core/transport" "v2ray.com/core/transport/pipe" @@ -265,7 +266,7 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport. } if d.router != nil && !skipRoutePick { - if tag, err := d.router.PickRoute(ctx); err == nil { + if tag, err := d.router.PickRoute(routing_session.AsRoutingContext(ctx)); err == nil { if h := d.ohm.GetHandler(tag); h != nil { newError("taking detour [", tag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx)) handler = h diff --git a/app/router/condition.go b/app/router/condition.go index ffafdb3c..5db1fa3c 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -10,10 +10,11 @@ import ( "v2ray.com/core/common/net" "v2ray.com/core/common/strmatcher" + "v2ray.com/core/features/routing" ) type Condition interface { - Apply(ctx *Context) bool + Apply(ctx routing.Context) bool } type ConditionChan []Condition @@ -28,7 +29,8 @@ func (v *ConditionChan) Add(cond Condition) *ConditionChan { return v } -func (v *ConditionChan) Apply(ctx *Context) bool { +// Apply applies all conditions registered in this chan. +func (v *ConditionChan) Apply(ctx routing.Context) bool { for _, cond := range *v { if !cond.Apply(ctx) { return false @@ -85,36 +87,18 @@ func (m *DomainMatcher) ApplyDomain(domain string) bool { return len(m.matchers.Match(domain)) > 0 } -func (m *DomainMatcher) Apply(ctx *Context) bool { - if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { +// Apply implements Condition. +func (m *DomainMatcher) Apply(ctx routing.Context) bool { + domain := ctx.GetTargetDomain() + if len(domain) == 0 { return false } - dest := ctx.Outbound.Target - if !dest.Address.Family().IsDomain() { - return false - } - return m.ApplyDomain(dest.Address.Domain()) -} - -func getIPsFromSource(ctx *Context) []net.IP { - if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { - return nil - } - dest := ctx.Inbound.Source - if dest.Address.Family().IsDomain() { - return nil - } - - return []net.IP{dest.Address.IP()} -} - -func getIPsFromTarget(ctx *Context) []net.IP { - return ctx.GetTargetIPs() + return m.ApplyDomain(domain) } type MultiGeoIPMatcher struct { matchers []*GeoIPMatcher - ipFunc func(*Context) []net.IP + onSource bool } func NewMultiGeoIPMatcher(geoips []*GeoIP, onSource bool) (*MultiGeoIPMatcher, error) { @@ -129,20 +113,20 @@ func NewMultiGeoIPMatcher(geoips []*GeoIP, onSource bool) (*MultiGeoIPMatcher, e matcher := &MultiGeoIPMatcher{ matchers: matchers, - } - - if onSource { - matcher.ipFunc = getIPsFromSource - } else { - matcher.ipFunc = getIPsFromTarget + onSource: onSource, } return matcher, nil } -func (m *MultiGeoIPMatcher) Apply(ctx *Context) bool { - ips := m.ipFunc(ctx) - +// Apply implements Condition. +func (m *MultiGeoIPMatcher) Apply(ctx routing.Context) bool { + var ips []net.IP + if m.onSource { + ips = ctx.GetSourceIPs() + } else { + ips = ctx.GetTargetIPs() + } for _, ip := range ips { for _, matcher := range m.matchers { if matcher.Match(ip) { @@ -166,20 +150,13 @@ func NewPortMatcher(list *net.PortList, onSource bool) *PortMatcher { } } -func (v *PortMatcher) Apply(ctx *Context) bool { - var port net.Port +// Apply implements Condition. +func (v *PortMatcher) Apply(ctx routing.Context) bool { if v.onSource { - if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { - return false - } - port = ctx.Inbound.Source.Port + return v.port.Contains(ctx.GetSourcePort()) } else { - if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { - return false - } - port = ctx.Outbound.Target.Port + return v.port.Contains(ctx.GetTargetPort()) } - return v.port.Contains(port) } type NetworkMatcher struct { @@ -194,11 +171,9 @@ func NewNetworkMatcher(network []net.Network) NetworkMatcher { return matcher } -func (v NetworkMatcher) Apply(ctx *Context) bool { - if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { - return false - } - return v.list[int(ctx.Outbound.Target.Network)] +// Apply implements Condition. +func (v NetworkMatcher) Apply(ctx routing.Context) bool { + return v.list[int(ctx.GetNetwork())] } type UserMatcher struct { @@ -217,17 +192,14 @@ func NewUserMatcher(users []string) *UserMatcher { } } -func (v *UserMatcher) Apply(ctx *Context) bool { - if ctx.Inbound == nil { - return false - } - - user := ctx.Inbound.User - if user == nil { +// Apply implements Condition. +func (v *UserMatcher) Apply(ctx routing.Context) bool { + user := ctx.GetUser() + if len(user) == 0 { return false } for _, u := range v.user { - if u == user.Email { + if u == user { return true } } @@ -250,11 +222,12 @@ func NewInboundTagMatcher(tags []string) *InboundTagMatcher { } } -func (v *InboundTagMatcher) Apply(ctx *Context) bool { - if ctx.Inbound == nil || len(ctx.Inbound.Tag) == 0 { +// Apply implements Condition. +func (v *InboundTagMatcher) Apply(ctx routing.Context) bool { + tag := ctx.GetInboundTag() + if len(tag) == 0 { return false } - tag := ctx.Inbound.Tag for _, t := range v.tags { if t == tag { return true @@ -281,18 +254,17 @@ func NewProtocolMatcher(protocols []string) *ProtocolMatcher { } } -func (m *ProtocolMatcher) Apply(ctx *Context) bool { - if ctx.Content == nil { +// Apply implements Condition. +func (m *ProtocolMatcher) Apply(ctx routing.Context) bool { + protocol := ctx.GetProtocol() + if len(protocol) == 0 { return false } - - protocol := ctx.Content.Protocol for _, p := range m.protocols { if strings.HasPrefix(protocol, p) { return true } } - return false } @@ -343,9 +315,11 @@ func (m *AttributeMatcher) Match(attrs map[string]interface{}) bool { return satisfied != nil && bool(satisfied.Truth()) } -func (m *AttributeMatcher) Apply(ctx *Context) bool { - if ctx.Content == nil { +// Apply implements Condition. +func (m *AttributeMatcher) Apply(ctx routing.Context) bool { + attributes := ctx.GetAttributes() + if attributes == nil { return false } - return m.Match(ctx.Content.Attributes) + return m.Match(attributes) } diff --git a/app/router/condition_geoip_test.go b/app/router/condition_geoip_test.go index 38e05373..c92d990b 100644 --- a/app/router/condition_geoip_test.go +++ b/app/router/condition_geoip_test.go @@ -17,8 +17,12 @@ func init() { wd, err := os.Getwd() common.Must(err) - common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat"))) - common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat"))) + if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && os.IsNotExist(err) { + common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat"))) + } + if _, err := os.Stat(platform.GetAssetLocation("geosite.dat")); err != nil && os.IsNotExist(err) { + common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat"))) + } } func TestGeoIPMatcherContainer(t *testing.T) { diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 20ead4ba..caef7367 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -17,27 +17,41 @@ import ( "v2ray.com/core/common/protocol" "v2ray.com/core/common/protocol/http" "v2ray.com/core/common/session" + "v2ray.com/core/features/routing" + routing_session "v2ray.com/core/features/routing/session" ) func init() { wd, err := os.Getwd() common.Must(err) - common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat"))) - common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat"))) + if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && os.IsNotExist(err) { + common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat"))) + } + if _, err := os.Stat(platform.GetAssetLocation("geosite.dat")); err != nil && os.IsNotExist(err) { + common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat"))) + } +} + +func withBackground() routing.Context { + return &routing_session.Context{} +} + +func withOutbound(outbound *session.Outbound) routing.Context { + return &routing_session.Context{Outbound: outbound} } -func withOutbound(outbound *session.Outbound) *Context { - return &Context{Outbound: outbound} +func withInbound(inbound *session.Inbound) routing.Context { + return &routing_session.Context{Inbound: inbound} } -func withInbound(inbound *session.Inbound) *Context { - return &Context{Inbound: inbound} +func withContent(content *session.Content) routing.Context { + return &routing_session.Context{Content: content} } func TestRoutingRule(t *testing.T) { type ruleTest struct { - input *Context + input routing.Context output bool } @@ -88,7 +102,7 @@ func TestRoutingRule(t *testing.T) { output: false, }, { - input: &Context{}, + input: withBackground(), output: false, }, }, @@ -124,7 +138,7 @@ func TestRoutingRule(t *testing.T) { output: true, }, { - input: &Context{}, + input: withBackground(), output: false, }, }, @@ -164,7 +178,7 @@ func TestRoutingRule(t *testing.T) { output: true, }, { - input: &Context{}, + input: withBackground(), output: false, }, }, @@ -205,7 +219,7 @@ func TestRoutingRule(t *testing.T) { output: false, }, { - input: &Context{}, + input: withBackground(), output: false, }, }, @@ -216,7 +230,7 @@ func TestRoutingRule(t *testing.T) { }, test: []ruleTest{ { - input: &Context{Content: &session.Content{Protocol: (&http.SniffHeader{}).Protocol()}}, + input: withContent(&session.Content{Protocol: (&http.SniffHeader{}).Protocol()}), output: true, }, }, @@ -299,7 +313,7 @@ func TestRoutingRule(t *testing.T) { }, test: []ruleTest{ { - input: &Context{Content: &session.Content{Protocol: "http/1.1", Attributes: map[string]interface{}{":path": "/test/1"}}}, + input: withContent(&session.Content{Protocol: "http/1.1", Attributes: map[string]interface{}{":path": "/test/1"}}), output: true, }, }, diff --git a/app/router/config.go b/app/router/config.go index 932fcd85..8eb9d5aa 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -5,6 +5,7 @@ package router import ( "v2ray.com/core/common/net" "v2ray.com/core/features/outbound" + "v2ray.com/core/features/routing" ) // CIDRList is an alias of []*CIDR to provide sort.Interface. @@ -59,7 +60,8 @@ func (r *Rule) GetTag() (string, error) { return r.Tag, nil } -func (r *Rule) Apply(ctx *Context) bool { +// Apply checks rule matching of current routing context. +func (r *Rule) Apply(ctx routing.Context) bool { return r.Condition.Apply(ctx) } diff --git a/app/router/router.go b/app/router/router.go index 542ff3d7..7e04c554 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -10,7 +10,6 @@ import ( "v2ray.com/core" "v2ray.com/core/common" "v2ray.com/core/common/net" - "v2ray.com/core/common/session" "v2ray.com/core/features/dns" "v2ray.com/core/features/outbound" "v2ray.com/core/features/routing" @@ -74,7 +73,8 @@ func (r *Router) Init(config *Config, d dns.Client, ohm outbound.Manager) error return nil } -func (r *Router) PickRoute(ctx context.Context) (string, error) { +// PickRoute implements routing.Router. +func (r *Router) PickRoute(ctx routing.Context) (string, error) { rule, err := r.pickRouteInternal(ctx) if err != nil { return "", err @@ -82,37 +82,26 @@ func (r *Router) PickRoute(ctx context.Context) (string, error) { return rule.GetTag() } -func isDomainOutbound(outbound *session.Outbound) bool { - return outbound != nil && outbound.Target.IsValid() && outbound.Target.Address.Family().IsDomain() -} - -// PickRoute implements routing.Router. -func (r *Router) pickRouteInternal(ctx context.Context) (*Rule, error) { - sessionContext := &Context{ - Inbound: session.InboundFromContext(ctx), - Outbound: session.OutboundFromContext(ctx), - Content: session.ContentFromContext(ctx), - } - +func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, error) { if r.domainStrategy == Config_IpOnDemand { - sessionContext.dnsClient = r.dns + ctx = ContextWithDNSClient(ctx, r.dns) } for _, rule := range r.rules { - if rule.Apply(sessionContext) { + if rule.Apply(ctx) { return rule, nil } } - if r.domainStrategy != Config_IpIfNonMatch || !isDomainOutbound(sessionContext.Outbound) { + if r.domainStrategy != Config_IpIfNonMatch || len(ctx.GetTargetDomain()) == 0 { return nil, common.ErrNoClue } - sessionContext.dnsClient = r.dns + ctx = ContextWithDNSClient(ctx, r.dns) // Try applying rules again if we have IPs. for _, rule := range r.rules { - if rule.Apply(sessionContext) { + if rule.Apply(ctx) { return rule, nil } } @@ -135,32 +124,30 @@ func (*Router) Type() interface{} { return routing.RouterType() } -type Context struct { - Inbound *session.Inbound - Outbound *session.Outbound - Content *session.Content - - dnsClient dns.Client +// ContextWithDNSClient creates a new routing context with domain resolving capability. Resolved domain IPs can be retrieved by GetTargetIPs(). +func ContextWithDNSClient(ctx routing.Context, client dns.Client) routing.Context { + return &resolvableContext{Context: ctx, dnsClient: client} } -func (c *Context) GetTargetIPs() []net.IP { - if c.Outbound == nil || !c.Outbound.Target.IsValid() { - return nil - } +type resolvableContext struct { + routing.Context + dnsClient dns.Client + resolvedIPs []net.IP +} - if c.Outbound.Target.Address.Family().IsIP() { - return []net.IP{c.Outbound.Target.Address.IP()} +func (ctx *resolvableContext) GetTargetIPs() []net.IP { + if ips := ctx.Context.GetTargetIPs(); len(ips) != 0 { + return ips } - if len(c.Outbound.ResolvedIPs) > 0 { - return c.Outbound.ResolvedIPs + if len(ctx.resolvedIPs) > 0 { + return ctx.resolvedIPs } - if c.dnsClient != nil { - domain := c.Outbound.Target.Address.Domain() - ips, err := c.dnsClient.LookupIP(domain) + if domain := ctx.GetTargetDomain(); len(domain) != 0 { + ips, err := ctx.dnsClient.LookupIP(domain) if err == nil { - c.Outbound.ResolvedIPs = ips + ctx.resolvedIPs = ips return ips } newError("resolve ip for ", domain).Base(err).WriteToLog() diff --git a/app/router/router_test.go b/app/router/router_test.go index 0992e1c9..0ed5f033 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -10,6 +10,7 @@ import ( "v2ray.com/core/common/net" "v2ray.com/core/common/session" "v2ray.com/core/features/outbound" + routing_session "v2ray.com/core/features/routing/session" "v2ray.com/core/testing/mocks" ) @@ -44,7 +45,7 @@ func TestSimpleRouter(t *testing.T) { })) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(ctx) + tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) if tag != "test" { t.Error("expect tag 'test', bug actually ", tag) @@ -85,7 +86,7 @@ func TestSimpleBalancer(t *testing.T) { })) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(ctx) + tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) if tag != "test" { t.Error("expect tag 'test', bug actually ", tag) @@ -120,7 +121,7 @@ func TestIPOnDemand(t *testing.T) { common.Must(r.Init(config, mockDns, nil)) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(ctx) + tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) if tag != "test" { t.Error("expect tag 'test', bug actually ", tag) @@ -155,7 +156,7 @@ func TestIPIfNonMatchDomain(t *testing.T) { common.Must(r.Init(config, mockDns, nil)) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(ctx) + tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) if tag != "test" { t.Error("expect tag 'test', bug actually ", tag) @@ -189,7 +190,7 @@ func TestIPIfNonMatchIP(t *testing.T) { common.Must(r.Init(config, mockDns, nil)) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 80)}) - tag, err := r.PickRoute(ctx) + tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) if tag != "test" { t.Error("expect tag 'test', bug actually ", tag) diff --git a/app/stats/command/command.go b/app/stats/command/command.go index 0423e533..21c85446 100644 --- a/app/stats/command/command.go +++ b/app/stats/command/command.go @@ -63,7 +63,7 @@ func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest return nil, newError("QueryStats only works its own stats.Manager.") } - manager.Visit(func(name string, c feature_stats.Counter) bool { + manager.VisitCounters(func(name string, c feature_stats.Counter) bool { if matcher.Match(name) { var value int64 if request.Reset_ { diff --git a/app/stats/stats.go b/app/stats/stats.go index c7f58831..fc9684d6 100644 --- a/app/stats/stats.go +++ b/app/stats/stats.go @@ -8,6 +8,7 @@ import ( "context" "sync" "sync/atomic" + "time" "v2ray.com/core/features/stats" ) @@ -32,15 +33,76 @@ func (c *Counter) Add(delta int64) int64 { return atomic.AddInt64(&c.value, delta) } +// Channel is an implementation of stats.Channel +type Channel struct { + channel chan interface{} + subscribers []chan interface{} + access sync.RWMutex +} + +// Channel implements stats.Channel +func (c *Channel) Channel() chan interface{} { + return c.channel +} + +// Subscribers implements stats.Channel +func (c *Channel) Subscribers() []chan interface{} { + c.access.RLock() + defer c.access.RUnlock() + return c.subscribers +} + +// Subscribe implements stats.Channel +func (c *Channel) Subscribe() chan interface{} { + c.access.Lock() + defer c.access.Unlock() + ch := make(chan interface{}) + c.subscribers = append(c.subscribers, ch) + return ch +} + +// Unsubscribe implements stats.Channel +func (c *Channel) Unsubscribe(ch chan interface{}) { + c.access.Lock() + defer c.access.Unlock() + for i, s := range c.subscribers { + if s == ch { + // Copy to new memory block to prevent modifying original data + subscribers := make([]chan interface{}, len(c.subscribers)-1) + copy(subscribers[:i], c.subscribers[:i]) + copy(subscribers[i:], c.subscribers[i+1:]) + c.subscribers = subscribers + return + } + } +} + +// Start starts the channel for listening to messsages +func (c *Channel) Start() { + for message := range c.Channel() { + subscribers := c.Subscribers() // Store a copy of slice value for concurrency safety + for _, sub := range subscribers { + select { + case sub <- message: // Successfully sent message + case <-time.After(100 * time.Millisecond): + c.Unsubscribe(sub) // Remove timeout subscriber + close(sub) // Actively close subscriber as notification + } + } + } +} + // Manager is an implementation of stats.Manager. type Manager struct { access sync.RWMutex counters map[string]*Counter + channels map[string]*Channel } func NewManager(ctx context.Context, config *Config) (*Manager, error) { m := &Manager{ counters: make(map[string]*Counter), + channels: make(map[string]*Channel), } return m, nil @@ -50,6 +112,7 @@ func (*Manager) Type() interface{} { return stats.ManagerType() } +// RegisterCounter implements stats.Manager. func (m *Manager) RegisterCounter(name string) (stats.Counter, error) { m.access.Lock() defer m.access.Unlock() @@ -63,18 +126,7 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) { return c, nil } -func (m *Manager) UnregisterCounter(name string) error { - m.access.Lock() - defer m.access.Unlock() - - if _, found := m.counters[name]; !found { - return newError("Counter ", name, " was not found.") - } - newError("remove counter ", name).AtDebug().WriteToLog() - delete(m.counters, name) - return nil -} - +// GetCounter implements stats.Manager. func (m *Manager) GetCounter(name string) stats.Counter { m.access.RLock() defer m.access.RUnlock() @@ -85,7 +137,8 @@ func (m *Manager) GetCounter(name string) stats.Counter { return nil } -func (m *Manager) Visit(visitor func(string, stats.Counter) bool) { +// VisitCounters calls visitor function on all managed counters. +func (m *Manager) VisitCounters(visitor func(string, stats.Counter) bool) { m.access.RLock() defer m.access.RUnlock() @@ -96,6 +149,32 @@ func (m *Manager) Visit(visitor func(string, stats.Counter) bool) { } } +// RegisterChannel implements stats.Manager. +func (m *Manager) RegisterChannel(name string) (stats.Channel, error) { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.channels[name]; found { + return nil, newError("Channel ", name, " already registered.") + } + newError("create new channel ", name).AtDebug().WriteToLog() + c := &Channel{channel: make(chan interface{})} + m.channels[name] = c + go c.Start() + return c, nil +} + +// GetChannel implements stats.Manager. +func (m *Manager) GetChannel(name string) stats.Channel { + m.access.RLock() + defer m.access.RUnlock() + + if c, found := m.channels[name]; found { + return c + } + return nil +} + // Start implements common.Runnable. func (m *Manager) Start() error { return nil @@ -105,3 +184,4 @@ func (m *Manager) Start() error { func (m *Manager) Close() error { return nil } + diff --git a/app/stats/stats_test.go b/app/stats/stats_test.go index 2a0d9b91..0c724257 100644 --- a/app/stats/stats_test.go +++ b/app/stats/stats_test.go @@ -2,14 +2,16 @@ package stats_test import ( "context" + "fmt" "testing" + "time" . "v2ray.com/core/app/stats" "v2ray.com/core/common" "v2ray.com/core/features/stats" ) -func TestInternface(t *testing.T) { +func TestInterface(t *testing.T) { _ = (stats.Manager)(new(Manager)) } @@ -33,3 +35,317 @@ func TestStatsCounter(t *testing.T) { t.Fatal("unexpected Value() return: ", v, ", wanted ", 0) } } + +func TestStatsChannel(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + c, err := m.RegisterChannel("test.channel") + common.Must(err) + + source := c.Channel() + a := c.Subscribe() + b := c.Subscribe() + defer c.Unsubscribe(a) + defer c.Unsubscribe(b) + + stopCh := make(chan struct{}) + errCh := make(chan string) + + go func() { + source <- 1 + source <- 2 + source <- "3" + source <- []int{4} + source <- nil // Dummy messsage with no subscriber receiving + select { + case source <- nil: // Source should be blocked here, for last message was not cleared + errCh <- fmt.Sprint("unexpected non-blocked source") + default: + close(stopCh) + } + }() + + go func() { + if v, ok := (<-a).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-a).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + if v, ok := (<-a).(string); !ok || v != "3" { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", "3") + } + if v, ok := (<-a).([]int); !ok || v[0] != 4 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", []int{4}) + } + }() + + go func() { + if v, ok := (<-b).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-b).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + if v, ok := (<-b).(string); !ok || v != "3" { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", "3") + } + if v, ok := (<-b).([]int); !ok || v[0] != 4 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", []int{4}) + } + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} + +func TestStatsChannelUnsubcribe(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + c, err := m.RegisterChannel("test.channel") + common.Must(err) + + source := c.Channel() + a := c.Subscribe() + b := c.Subscribe() + defer c.Unsubscribe(a) + + pauseCh := make(chan struct{}) + stopCh := make(chan struct{}) + errCh := make(chan string) + + { + var aSet, bSet bool + for _, s := range c.Subscribers() { + if s == a { + aSet = true + } + if s == b { + bSet = true + } + } + if !(aSet && bSet) { + t.Fatal("unexpected subscribers: ", c.Subscribers()) + } + } + + go func() { + source <- 1 + <-pauseCh // Wait for `b` goroutine to resume sending message + source <- 2 + }() + + go func() { + if v, ok := (<-a).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-a).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + }() + + go func() { + if v, ok := (<-b).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + // Unsubscribe `b` while `source`'s messaging is paused + c.Unsubscribe(b) + { // Test `b` is not in subscribers + var aSet, bSet bool + for _, s := range c.Subscribers() { + if s == a { + aSet = true + } + if s == b { + bSet = true + } + } + if !(aSet && !bSet) { + errCh <- fmt.Sprint("unexpected subscribers: ", c.Subscribers()) + } + } + // Resume `source`'s progress + close(pauseCh) + // Test `b` is neither closed nor able to receive any data + select { + case v, ok := <-b: + if ok { + errCh <- fmt.Sprint("unexpected data received: ", v) + } else { + errCh <- fmt.Sprint("unexpected closed channel: ", b) + } + default: + } + close(stopCh) + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} + +func TestStatsChannelTimeout(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + c, err := m.RegisterChannel("test.channel") + common.Must(err) + + source := c.Channel() + a := c.Subscribe() + b := c.Subscribe() + defer c.Unsubscribe(a) + defer c.Unsubscribe(b) + + stopCh := make(chan struct{}) + errCh := make(chan string) + + go func() { + source <- 1 + source <- 2 + }() + + go func() { + if v, ok := (<-a).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-a).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + { // Test `b` is still in subscribers yet (because `a` receives 2 first) + var aSet, bSet bool + for _, s := range c.Subscribers() { + if s == a { + aSet = true + } + if s == b { + bSet = true + } + } + if !(aSet && bSet) { + errCh <- fmt.Sprint("unexpected subscribers: ", c.Subscribers()) + } + } + }() + + go func() { + if v, ok := (<-b).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + // Block `b` channel for a time longer than `source`'s timeout + <-time.After(150 * time.Millisecond) + { // Test `b` has been unsubscribed by source + var aSet, bSet bool + for _, s := range c.Subscribers() { + if s == a { + aSet = true + } + if s == b { + bSet = true + } + } + if !(aSet && !bSet) { + errCh <- fmt.Sprint("unexpected subscribers: ", c.Subscribers()) + } + } + select { // Test `b` has been closed by source + case v, ok := <-b: + if ok { + errCh <- fmt.Sprint("unexpected data received: ", v) + } + default: + } + close(stopCh) + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} + +func TestStatsChannelConcurrency(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + c, err := m.RegisterChannel("test.channel") + common.Must(err) + + source := c.Channel() + a := c.Subscribe() + b := c.Subscribe() + defer c.Unsubscribe(a) + + stopCh := make(chan struct{}) + errCh := make(chan string) + + go func() { + source <- 1 + source <- 2 + }() + + go func() { + if v, ok := (<-a).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-a).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + }() + + go func() { + // Block `b` for a time shorter than `source`'s timeout + // So as to ensure source channel is trying to send message to `b`. + <-time.After(25 * time.Millisecond) + // This causes concurrency scenario: unsubscribe `b` while trying to send message to it + c.Unsubscribe(b) + // Test `b` is not closed and can still receive data 1: + // Because unsubscribe won't affect the ongoing process of sending message. + select { + case v, ok := <-b: + if v1, ok1 := v.(int); !(ok && ok1 && v1 == 1) { + errCh <- fmt.Sprint("unexpected failure in receiving data: ", 1) + } + default: + errCh <- fmt.Sprint("unexpected block from receiving data: ", 1) + } + // Test `b` is not closed but cannot receive data 2: + // Becuase in a new round of messaging, `b` has been unsubscribed. + select { + case v, ok := <-b: + if ok { + errCh <- fmt.Sprint("unexpected receving: ", v) + } else { + errCh <- fmt.Sprint("unexpected closing of channel") + } + default: + } + close(stopCh) + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8fb28873..ae3f0810 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,7 +20,7 @@ steps: - checkout: self - task: GoTool@0 inputs: - version: '1.14.7' + version: '1.15.1' - script: | mkdir triggersrc ls -I "triggersrc" | xargs cp -rf -t triggersrc diff --git a/common/session/session.go b/common/session/session.go index 86172c8a..8d7b1ff6 100644 --- a/common/session/session.go +++ b/common/session/session.go @@ -51,8 +51,6 @@ type Outbound struct { Target net.Destination // Gateway address Gateway net.Address - // ResolvedIPs is the resolved IP addresses, if the Targe is a domain address. - ResolvedIPs []net.IP } type SniffingRequest struct { diff --git a/features/routing/context.go b/features/routing/context.go new file mode 100644 index 00000000..b4adabaf --- /dev/null +++ b/features/routing/context.go @@ -0,0 +1,40 @@ +package routing + +import ( + "v2ray.com/core/common/net" +) + +// Context is a feature to store connection information for routing. +// +// v2ray:api:beta +type Context interface { + // GetInboundTag returns the tag of the inbound the connection was from. + GetInboundTag() string + + // GetSourcesIPs returns the source IPs bound to the connection. + GetSourceIPs() []net.IP + + // GetSourcePort returns the source port of the connection. + GetSourcePort() net.Port + + // GetTargetIPs returns the target IP of the connection or resolved IPs of target domain. + GetTargetIPs() []net.IP + + // GetTargetPort returns the target port of the connection. + GetTargetPort() net.Port + + // GetTargetDomain returns the target domain of the connection, if exists. + GetTargetDomain() string + + // GetNetwork returns the network type of the connection. + GetNetwork() net.Network + + // GetProtocol returns the protocol from the connection content, if sniffed out. + GetProtocol() string + + // GetUser returns the user email from the connection content, if exists. + GetUser() string + + // GetAttributes returns extra attributes from the conneciont content. + GetAttributes() map[string]interface{} +} diff --git a/features/routing/router.go b/features/routing/router.go index a71927b8..f473431a 100644 --- a/features/routing/router.go +++ b/features/routing/router.go @@ -1,20 +1,18 @@ package routing import ( - "context" - "v2ray.com/core/common" "v2ray.com/core/features" ) // Router is a feature to choose an outbound tag for the given request. // -// v2ray:api:stable +// v2ray:api:beta type Router interface { features.Feature // PickRoute returns a tag of an OutboundHandler based on the given context. - PickRoute(ctx context.Context) (string, error) + PickRoute(ctx Context) (string, error) } // RouterType return the type of Router interface. Can be used to implement common.HasType. @@ -33,7 +31,7 @@ func (DefaultRouter) Type() interface{} { } // PickRoute implements Router. -func (DefaultRouter) PickRoute(ctx context.Context) (string, error) { +func (DefaultRouter) PickRoute(ctx Context) (string, error) { return "", common.ErrNoClue } diff --git a/features/routing/session/context.go b/features/routing/session/context.go new file mode 100644 index 00000000..6d61d4f9 --- /dev/null +++ b/features/routing/session/context.go @@ -0,0 +1,119 @@ +package session + +import ( + "context" + + "v2ray.com/core/common/net" + "v2ray.com/core/common/session" + "v2ray.com/core/features/routing" +) + +// Context is an implementation of routing.Context, which is a wrapper of context.context with session info. +type Context struct { + Inbound *session.Inbound + Outbound *session.Outbound + Content *session.Content +} + +// GetInboundTag implements routing.Context. +func (ctx *Context) GetInboundTag() string { + if ctx.Inbound == nil { + return "" + } + return ctx.Inbound.Tag +} + +// GetSourceIPs implements routing.Context. +func (ctx *Context) GetSourceIPs() []net.IP { + if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { + return nil + } + dest := ctx.Inbound.Source + if dest.Address.Family().IsDomain() { + return nil + } + + return []net.IP{dest.Address.IP()} +} + +// GetSourcePort implements routing.Context. +func (ctx *Context) GetSourcePort() net.Port { + if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { + return 0 + } + return ctx.Inbound.Source.Port +} + +// GetTargetIPs implements routing.Context. +func (ctx *Context) GetTargetIPs() []net.IP { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return nil + } + + if ctx.Outbound.Target.Address.Family().IsIP() { + return []net.IP{ctx.Outbound.Target.Address.IP()} + } + + return nil +} + +// GetTargetPort implements routing.Context. +func (ctx *Context) GetTargetPort() net.Port { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return 0 + } + return ctx.Outbound.Target.Port +} + +// GetTargetDomain implements routing.Context. +func (ctx *Context) GetTargetDomain() string { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return "" + } + dest := ctx.Outbound.Target + if !dest.Address.Family().IsDomain() { + return "" + } + return dest.Address.Domain() +} + +// GetNetwork implements routing.Context. +func (ctx *Context) GetNetwork() net.Network { + if ctx.Outbound == nil { + return net.Network_Unknown + } + return ctx.Outbound.Target.Network +} + +// GetProtocol implements routing.Context. +func (ctx *Context) GetProtocol() string { + if ctx.Content == nil { + return "" + } + return ctx.Content.Protocol +} + +// GetUser implements routing.Context. +func (ctx *Context) GetUser() string { + if ctx.Inbound == nil { + return "" + } + return ctx.Inbound.User.Email +} + +// GetAttributes implements routing.Context. +func (ctx *Context) GetAttributes() map[string]interface{} { + if ctx.Content == nil { + return nil + } + return ctx.Content.Attributes +} + +// AsRoutingContext creates a context from context.context with session info. +func AsRoutingContext(ctx context.Context) routing.Context { + return &Context{ + Inbound: session.InboundFromContext(ctx), + Outbound: session.OutboundFromContext(ctx), + Content: session.ContentFromContext(ctx), + } +} diff --git a/features/stats/stats.go b/features/stats/stats.go index abc2ba18..6ac0da26 100644 --- a/features/stats/stats.go +++ b/features/stats/stats.go @@ -16,6 +16,20 @@ type Counter interface { Add(int64) int64 } +// Channel is the interface for stats channel +// +// v2ray:api:stable +type Channel interface { + // Channel returns the underlying go channel. + Channel() chan interface{} + // SubscriberCount returns the number of the subscribers. + Subscribers() []chan interface{} + // Subscribe registers for listening to channel stream and returns a new listener channel. + Subscribe() chan interface{} + // Unsubscribe unregisters a listener channel from current Channel object. + Unsubscribe(chan interface{}) +} + // Manager is the interface for stats manager. // // v2ray:api:stable @@ -27,6 +41,11 @@ type Manager interface { UnregisterCounter(string) error // GetCounter returns a counter by its identifier. GetCounter(string) Counter + + // RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels. + RegisterChannel(string) (Channel, error) + // GetChannel returns a channel by its identifier. + GetChannel(string) Channel } // GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter. @@ -39,6 +58,16 @@ func GetOrRegisterCounter(m Manager, name string) (Counter, error) { return m.RegisterCounter(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. // // v2ray:api:stable @@ -69,6 +98,16 @@ func (NoopManager) GetCounter(string) Counter { return nil } +// RegisterChannel implements Manager. +func (NoopManager) RegisterChannel(string) (Channel, error) { + return nil, newError("not implemented") +} + +// GetChannel implements Manager. +func (NoopManager) GetChannel(string) Channel { + return nil +} + // Start implements common.Runnable. func (NoopManager) Start() error { return nil } diff --git a/go.mod b/go.mod index c03d4064..61a94558 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,9 @@ module v2ray.com/core +go 1.15 + require ( - github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect + github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 // indirect github.com/golang/mock v1.4.4 github.com/golang/protobuf v1.4.2 github.com/google/go-cmp v0.5.2 @@ -11,14 +13,12 @@ require ( github.com/seiflotfy/cuckoofilter v0.0.0-20200511222245-56093a4d3841 github.com/stretchr/testify v1.6.1 github.com/xiaokangwang/VSign v0.0.0-20200828155424-dc1c86b73fbf - go.starlark.net v0.0.0-20190919145610-979af19b165c - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20200602114024-627f9648deb9 - golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a - golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd + go.starlark.net v0.0.0-20200901195727-6e684ef5eeee + golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a + golang.org/x/net v0.0.0-20200822124328-c89045814202 + golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 + golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a google.golang.org/grpc v1.31.1 google.golang.org/protobuf v1.25.0 h12.io/socks v1.0.1 ) - -go 1.13 diff --git a/go.sum b/go.sum index eb8f1678..8c4942a7 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,15 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= -github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E= +github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a h1:YtdtTUN1iH97s+6PUjLnaiKSQj4oG1/EZ3N9bx6g4kU= github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a/go.mod h1:/CZpbhAusDOobpcb9yubw46kdYjq0zRC0Wpg9a9zFQM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -55,12 +58,13 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xiaokangwang/VSign v0.0.0-20200828155424-dc1c86b73fbf h1:d4keT3SwLbrgnEe2zbtijPLgKE15n0ZbvJZzRH/a9GM= github.com/xiaokangwang/VSign v0.0.0-20200828155424-dc1c86b73fbf/go.mod h1:jTwBnzBuqZP3VX/Z65ErYb9zd4anQprSC7N38TmAp1E= -go.starlark.net v0.0.0-20190919145610-979af19b165c h1:WR7X1xgXJlXhQBdorVc9Db3RhwG+J/kp6bLuMyJjfVw= -go.starlark.net v0.0.0-20190919145610-979af19b165c/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= +go.starlark.net v0.0.0-20200901195727-6e684ef5eeee h1:N4eRtIIYHZE5Mw/Km/orb+naLdwAe+lv2HCxRR5rEBw= +go.starlark.net v0.0.0-20200901195727-6e684ef5eeee/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU= 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.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -73,20 +77,22 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn 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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a h1:i47hUS795cOydZI4AwJQCKXOr4BvxzvikwDoDtHhP2Y= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index 1725af7c..ca5a037f 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -20,9 +20,12 @@ func init() { wd, err := os.Getwd() common.Must(err) - common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat"))) + if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && os.IsNotExist(err) { + common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat"))) + } - geositeFilePath := platform.GetAssetLocation("geosite.dat") + geositeFilePath := filepath.Join(wd, "geosite.dat") + os.Setenv("v2ray.location.asset", wd) geositeFile, err := os.OpenFile(geositeFilePath, os.O_CREATE|os.O_WRONLY, 0600) common.Must(err) defer geositeFile.Close() @@ -46,6 +49,7 @@ func TestDnsConfigParsing(t *testing.T) { geositePath := platform.GetAssetLocation("geosite.dat") defer func() { os.Remove(geositePath) + os.Unsetenv("v2ray.location.asset") }() parserCreator := func() func(string) (proto.Message, error) { diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 34f9aac7..14655fe5 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -219,14 +219,18 @@ func (c *QUICConfig) Build() (proto.Message, error) { } type DomainSocketConfig struct { - Path string `json:"path"` - Abstract bool `json:"abstract"` + Path string `json:"path"` + Abstract bool `json:"abstract"` + Padding bool `json:"padding"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` } func (c *DomainSocketConfig) Build() (proto.Message, error) { return &domainsocket.Config{ - Path: c.Path, - Abstract: c.Abstract, + Path: c.Path, + Abstract: c.Abstract, + Padding: c.Padding, + AcceptProxyProtocol: c.AcceptProxyProtocol, }, nil } diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 64d9e9af..03f4cb79 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -97,10 +97,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { fb.Type = "serve" } else { switch fb.Dest[0] { - case '@': - fb.Dest = "\x00" + fb.Dest[1:] - fallthrough - case '/': + case '@', '/': fb.Type = "unix" default: if _, err := strconv.Atoi(fb.Dest); err == nil { diff --git a/infra/conf/vless_test.go b/infra/conf/vless_test.go index ac7124c3..12035095 100644 --- a/infra/conf/vless_test.go +++ b/infra/conf/vless_test.go @@ -113,7 +113,7 @@ func TestVLessInbound(t *testing.T) { Alpn: "h2", Path: "", Type: "unix", - Dest: "\x00/dev/shm/domain.socket", + Dest: "@/dev/shm/domain.socket", Xver: 2, }, { diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index d02a534b..0839d6f6 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -260,11 +260,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection i } return nil }); err != nil { - dest := fb.Dest - if dest[0] == '\x00' { - dest = "@" + dest[1:] - } - return newError("failed to dial to " + dest).Base(err).AtWarning() + return newError("failed to dial to " + fb.Dest).Base(err).AtWarning() } defer conn.Close() // nolint: errcheck diff --git a/release/config/systemd/system/v2ray.service b/release/config/systemd/system/v2ray.service index 799dc7e8..a299209c 100644 --- a/release/config/systemd/system/v2ray.service +++ b/release/config/systemd/system/v2ray.service @@ -10,6 +10,7 @@ AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true ExecStart=/usr/local/bin/v2ray -config /usr/local/etc/v2ray/config.json Restart=on-failure +RestartPreventExitStatus=23 [Install] WantedBy=multi-user.target diff --git a/release/config/systemd/system/v2ray@.service b/release/config/systemd/system/v2ray@.service index d3cc518a..514a86aa 100644 --- a/release/config/systemd/system/v2ray@.service +++ b/release/config/systemd/system/v2ray@.service @@ -10,6 +10,7 @@ AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true ExecStart=/usr/local/bin/v2ray -config /usr/local/etc/v2ray/%i.json Restart=on-failure +RestartPreventExitStatus=23 [Install] WantedBy=multi-user.target diff --git a/transport/internet/domainsocket/config.go b/transport/internet/domainsocket/config.go index 09cfabb2..65216212 100644 --- a/transport/internet/domainsocket/config.go +++ b/transport/internet/domainsocket/config.go @@ -9,14 +9,23 @@ import ( ) const protocolName = "domainsocket" +const sizeofSunPath = 108 func (c *Config) GetUnixAddr() (*net.UnixAddr, error) { path := c.Path if path == "" { return nil, newError("empty domain socket path") } - if c.Abstract && path[0] != '\x00' { - path = "\x00" + path + if c.Abstract && path[0] != '@' { + path = "@" + path + } + if c.Abstract && c.Padding { + raw := []byte(path) + addr := make([]byte, sizeofSunPath) + for i, c := range raw { + addr[i] = c + } + path = string(addr) } return &net.UnixAddr{ Name: path, diff --git a/transport/internet/domainsocket/config.pb.go b/transport/internet/domainsocket/config.pb.go index 722ffe21..76c61b15 100644 --- a/transport/internet/domainsocket/config.pb.go +++ b/transport/internet/domainsocket/config.pb.go @@ -30,11 +30,17 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Path of the domain socket. This overrides the IP/Port parameter from upstream caller. + // Path of the domain socket. This overrides the IP/Port parameter from + // upstream caller. Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // Abstract speicifies whether to use abstract namespace or not. - // Traditionally Unix domain socket is file system based. Abstract domain socket can be used without acquiring file lock. + // Traditionally Unix domain socket is file system based. Abstract domain + // socket can be used without acquiring file lock. Abstract bool `protobuf:"varint,2,opt,name=abstract,proto3" json:"abstract,omitempty"` + // Some apps, eg. haproxy, use the full length of sockaddr_un.sun_path to + // connect(2) or bind(2) when using abstract UDS. + Padding bool `protobuf:"varint,3,opt,name=padding,proto3" json:"padding,omitempty"` + AcceptProxyProtocol bool `protobuf:"varint,4,opt,name=acceptProxyProtocol,proto3" json:"acceptProxyProtocol,omitempty"` } func (x *Config) Reset() { @@ -83,6 +89,20 @@ func (x *Config) GetAbstract() bool { return false } +func (x *Config) GetPadding() bool { + if x != nil { + return x.Padding + } + return false +} + +func (x *Config) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + var File_transport_internet_domainsocket_config_proto protoreflect.FileDescriptor var file_transport_internet_domainsocket_config_proto_rawDesc = []byte{ @@ -91,20 +111,25 @@ var file_transport_internet_domainsocket_config_proto_rawDesc = []byte{ 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2a, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x22, 0x38, 0x0a, 0x06, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x62, 0x73, 0x74, - 0x72, 0x61, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x62, 0x73, 0x74, - 0x72, 0x61, 0x63, 0x74, 0x42, 0x8f, 0x01, 0x0a, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, - 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x50, 0x01, 0x5a, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, - 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0xaa, 0x02, 0x2a, 0x56, 0x32, 0x52, 0x61, - 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x22, 0x84, 0x01, 0x0a, 0x06, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x62, 0x73, + 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x62, 0x73, + 0x74, 0x72, 0x61, 0x63, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x12, + 0x30, 0x0a, 0x13, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x42, 0x8f, 0x01, 0x0a, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x6f, + 0x63, 0x6b, 0x65, 0x74, 0x50, 0x01, 0x5a, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0xaa, 0x02, 0x2a, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, + 0x6f, 0x72, 0x65, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x6f, 0x63, + 0x6b, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/transport/internet/domainsocket/config.proto b/transport/internet/domainsocket/config.proto index b79565bd..5c1086d0 100644 --- a/transport/internet/domainsocket/config.proto +++ b/transport/internet/domainsocket/config.proto @@ -7,9 +7,15 @@ option java_package = "com.v2ray.core.transport.internet.domainsocket"; option java_multiple_files = true; message Config { - // Path of the domain socket. This overrides the IP/Port parameter from upstream caller. - string path = 1; - // Abstract speicifies whether to use abstract namespace or not. - // Traditionally Unix domain socket is file system based. Abstract domain socket can be used without acquiring file lock. - bool abstract = 2; + // Path of the domain socket. This overrides the IP/Port parameter from + // upstream caller. + string path = 1; + // Abstract speicifies whether to use abstract namespace or not. + // Traditionally Unix domain socket is file system based. Abstract domain + // socket can be used without acquiring file lock. + bool abstract = 2; + // Some apps, eg. haproxy, use the full length of sockaddr_un.sun_path to + // connect(2) or bind(2) when using abstract UDS. + bool padding = 3; + bool acceptProxyProtocol = 4; } diff --git a/transport/internet/domainsocket/listener.go b/transport/internet/domainsocket/listener.go index aba951d0..9a98971e 100644 --- a/transport/internet/domainsocket/listener.go +++ b/transport/internet/domainsocket/listener.go @@ -10,10 +10,12 @@ import ( "os" "strings" + "github.com/pires/go-proxyproto" "golang.org/x/sys/unix" "v2ray.com/core/common" "v2ray.com/core/common/net" + "v2ray.com/core/common/session" "v2ray.com/core/transport/internet" "v2ray.com/core/transport/internet/tls" ) @@ -39,11 +41,23 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti return nil, newError("failed to listen domain socket").Base(err).AtWarning() } - ln := &Listener{ - addr: addr, - ln: unixListener, - config: settings, - addConn: handler, + var ln *Listener + if settings.AcceptProxyProtocol { + policyFunc := func(upstream net.Addr) (proxyproto.Policy, error) { return proxyproto.REQUIRE, nil } + ln = &Listener{ + addr: addr, + ln: &proxyproto.Listener{Listener: unixListener, Policy: policyFunc}, + config: settings, + addConn: handler, + } + newError("accepting PROXY protocol").AtWarning().WriteToLog(session.ExportIDToError(ctx)) + } else { + ln = &Listener{ + addr: addr, + ln: unixListener, + config: settings, + addConn: handler, + } } if !settings.Abstract {