Test: add subscription server

This commit is contained in:
世界 2023-03-14 23:19:55 +08:00
parent c77681ea17
commit 4a60676e89
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
11 changed files with 414 additions and 59 deletions

View File

@ -17,6 +17,7 @@ import (
type Router interface {
Service
Inbound(tag string) (Inbound, bool)
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) Outbound

16
adapter/subscription.go Normal file
View File

@ -0,0 +1,16 @@
package adapter
type SubscriptionSupport interface {
GenerateSubscription(options GenerateSubscriptionOptions) ([]byte, error)
}
type GenerateSubscriptionOptions struct {
Format string
Application string
Remarks string
ServerAddress string
}
type SubscriptionServer interface {
Service
}

102
box.go
View File

@ -12,6 +12,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/experimental/subscription"
"github.com/sagernet/sing-box/inbound"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
@ -25,16 +26,16 @@ import (
var _ adapter.Service = (*Box)(nil)
type Box struct {
createdAt time.Time
router adapter.Router
inbounds []adapter.Inbound
outbounds []adapter.Outbound
logFactory log.Factory
logger log.ContextLogger
logFile *os.File
clashServer adapter.ClashServer
v2rayServer adapter.V2RayServer
done chan struct{}
createdAt time.Time
router adapter.Router
inbounds []adapter.Inbound
outbounds []adapter.Outbound
logFactory log.Factory
logger log.ContextLogger
logFile *os.File
preServices map[string]adapter.Service
postServices map[string]adapter.Service
done chan struct{}
}
func New(ctx context.Context, options option.Options, platformInterface platform.Interface) (*Box, error) {
@ -43,6 +44,7 @@ func New(ctx context.Context, options option.Options, platformInterface platform
var needClashAPI bool
var needV2RayAPI bool
var needSubscriptionServer bool
if options.Experimental != nil {
if options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
needClashAPI = true
@ -50,6 +52,9 @@ func New(ctx context.Context, options option.Options, platformInterface platform
if options.Experimental.V2RayAPI != nil && options.Experimental.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
if options.Experimental.Subscription != nil && options.Experimental.Subscription.Listen != "" {
needSubscriptionServer = true
}
}
var logFactory log.Factory
@ -165,33 +170,41 @@ func New(ctx context.Context, options option.Options, platformInterface platform
return nil, err
}
var clashServer adapter.ClashServer
var v2rayServer adapter.V2RayServer
preServices := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service)
if needClashAPI {
clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
clashServer, err := experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
if err != nil {
return nil, E.Cause(err, "create clash api server")
}
router.SetClashServer(clashServer)
preServices["clash api"] = clashServer
}
if needV2RayAPI {
v2rayServer, err = experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(options.Experimental.V2RayAPI))
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(options.Experimental.V2RayAPI))
if err != nil {
return nil, E.Cause(err, "create v2ray api server")
}
router.SetV2RayServer(v2rayServer)
preServices["v2ray api"] = v2rayServer
}
if needSubscriptionServer {
subscriptionServer, err := subscription.NewServer(ctx, router, logFactory.NewLogger("subscription"), common.PtrValueOrDefault(options.Experimental.Subscription))
if err != nil {
return nil, E.Cause(err, "create subscription server")
}
postServices["subscription"] = subscriptionServer
}
return &Box{
router: router,
inbounds: inbounds,
outbounds: outbounds,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
logFile: logFile,
clashServer: clashServer,
v2rayServer: v2rayServer,
done: make(chan struct{}),
router: router,
inbounds: inbounds,
outbounds: outbounds,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
logFile: logFile,
preServices: preServices,
postServices: postServices,
done: make(chan struct{}),
}, nil
}
@ -213,16 +226,10 @@ func (s *Box) Start() error {
}
func (s *Box) start() error {
if s.clashServer != nil {
err := s.clashServer.Start()
for name, preServer := range s.preServices {
err := preServer.Start()
if err != nil {
return E.Cause(err, "start clash api server")
}
}
if s.v2rayServer != nil {
err := s.v2rayServer.Start()
if err != nil {
return E.Cause(err, "start v2ray api server")
return E.Cause(err, "start ", name, " service")
}
}
for i, out := range s.outbounds {
@ -256,6 +263,13 @@ func (s *Box) start() error {
}
}
for name, service := range s.postServices {
err = service.Start()
if err != nil {
return E.Cause(err, "start ", name, " service")
}
}
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
return nil
}
@ -268,6 +282,11 @@ func (s *Box) Close() error {
close(s.done)
}
var errors error
for name, service := range s.postServices {
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", name, " service")
})
}
for i, in := range s.inbounds {
errors = E.Append(errors, in.Close(), func(err error) error {
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
@ -283,21 +302,16 @@ func (s *Box) Close() error {
return E.Cause(err, "close router")
})
}
for name, service := range s.preServices {
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", name, " service")
})
}
if err := common.Close(s.logFactory); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close log factory")
})
}
if err := common.Close(s.clashServer); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close clash api server")
})
}
if err := common.Close(s.v2rayServer); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close v2ray api server")
})
}
if s.logFile != nil {
errors = E.Append(errors, s.logFile.Close(), func(err error) error {
return E.Cause(err, "close log file")

View File

@ -63,6 +63,9 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
zap.InfoLevel,
)),
}
if config.DefaultServerName == "" {
config.DefaultServerName = options.Domain[0]
}
acmeConfig := certmagic.ACMEIssuer{
CA: acmeServer,
Email: options.Email,

39
common/tls/listener.go Normal file
View File

@ -0,0 +1,39 @@
package tls
import (
"context"
"net"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type Listener struct {
net.Listener
ctx context.Context
logger logger.Logger
config ServerConfig
}
func NewListener(ctx context.Context, logger logger.Logger, inner net.Listener, config ServerConfig) net.Listener {
return &Listener{
Listener: inner,
ctx: ctx,
logger: logger,
config: config,
}
}
func (l *Listener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
tlsConn, err := ServerHandshake(l.ctx, conn, l.config)
if err != nil {
l.logger.Error(E.Cause(err, "accept connection from ", conn.RemoteAddr(), ": "))
conn.Close()
return l.Accept()
}
return tlsConn, err
}

10
constant/subscription.go Normal file
View File

@ -0,0 +1,10 @@
package constant
const (
SubscriptionTypeRaw = "raw"
SubscriptionTypeSIP008 = "sip008"
)
const (
SubscriptionApplicationShadowrocket = "shadowrocket"
)

View File

@ -0,0 +1,172 @@
package subscription
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"net"
"net/http"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
sHttp "github.com/sagernet/sing/protocol/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"golang.org/x/net/http2"
)
type Server struct {
ctx context.Context
router adapter.Router
logger log.Logger
httpServer *http.Server
tlsConfig tls.ServerConfig
servers []ServerItem
}
type ServerItem struct {
Remarks string
ServerAddress string
Interface adapter.SubscriptionSupport
}
func NewServer(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.SubscriptionOptions) (adapter.SubscriptionServer, error) {
chiRouter := chi.NewRouter()
server := &Server{
ctx: ctx,
router: router,
logger: logger,
httpServer: &http.Server{
Addr: options.Listen,
Handler: chiRouter,
},
}
if options.TLS != nil && options.TLS.Enabled {
tlsConfig, err := tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
server.tlsConfig = tlsConfig
}
listenPrefix := options.ListenPrefix
if !strings.HasPrefix(listenPrefix, "/") {
listenPrefix = "/" + listenPrefix
}
chiRouter.Get("/", server.handleSubscription)
for i, serverOptions := range options.Servers {
serverAddress := serverOptions.ServerAddress
if serverAddress == "" {
serverAddress = options.ServerAddress
}
if serverAddress == "" {
return nil, E.New("parse servers[", i, "]: ", "missing server address")
}
if serverOptions.InboundTag != "" {
inbound, loaded := router.Inbound(serverOptions.InboundTag)
if !loaded {
return nil, E.New("parse servers[", i, "]: ", "inbound not found: ", serverOptions.InboundTag)
}
subscriptionSupport, loaded := inbound.(adapter.SubscriptionSupport)
if !loaded {
return nil, E.New("parse servers[", i, "]: ", "inbound have no subscription support: ", serverOptions.InboundTag)
}
server.servers = append(server.servers, ServerItem{
Remarks: serverOptions.Remarks,
ServerAddress: serverAddress,
Interface: subscriptionSupport,
})
}
}
return server, nil
}
func (s *Server) Start() error {
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return E.Cause(err, "subscription server listen error")
}
if s.tlsConfig != nil {
s.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
err = s.tlsConfig.Start()
if err != nil {
return err
}
listener = tls.NewListener(s.ctx, s.logger, listener, s.tlsConfig)
}
s.logger.Info("subscription server listening at ", listener.Addr())
go func() {
err = s.httpServer.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("subscription server serve error: ", err)
}
}()
return nil
}
func (s *Server) Close() error {
return common.Close(
common.PtrOrNil(s.httpServer),
s.tlsConfig,
)
}
func (s *Server) handleSubscription(writer http.ResponseWriter, request *http.Request) {
userAgent := request.Header.Get("User-Agent")
s.logger.Info("accepted request from ", sHttp.SourceAddress(request), " with User-Agent: ", userAgent)
subscriptionFormat := chi.URLParam(request, "type")
if subscriptionFormat == "" {
subscriptionFormat = C.SubscriptionTypeRaw
}
application := chi.URLParam(request, "application")
if application == "" {
if strings.Contains(userAgent, "Shadowrocket") {
application = C.SubscriptionApplicationShadowrocket
}
}
generateOptions := adapter.GenerateSubscriptionOptions{
Format: subscriptionFormat,
Application: application,
}
var contentList [][]byte
for _, serverItem := range s.servers {
generateOptions.Remarks = serverItem.Remarks
generateOptions.ServerAddress = serverItem.ServerAddress
content, err := serverItem.Interface.GenerateSubscription(generateOptions)
if err != nil {
// TODO: process error
continue
}
contentList = append(contentList, content)
}
switch subscriptionFormat {
case C.SubscriptionTypeSIP008:
render.JSON(writer, request, render.M{
"version": 1,
"servers": common.Map(contentList, func(content []byte) json.RawMessage {
return content
}),
})
case C.SubscriptionTypeRaw:
rawConfig := strings.Join(common.Map(contentList, func(it []byte) string { return string(it) }), "\n")
render.PlainText(writer, request, base64.URLEncoding.EncodeToString([]byte(rawConfig)))
default:
render.Status(request, http.StatusBadRequest)
render.PlainText(writer, request, "unsupported subscription format: "+subscriptionFormat)
}
}

79
inbound/subscription.go Normal file
View File

@ -0,0 +1,79 @@
package inbound
import (
"encoding/base64"
"net/url"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid"
)
var _ adapter.SubscriptionSupport = (*Shadowsocks)(nil)
func (h *Shadowsocks) GenerateSubscription(options adapter.GenerateSubscriptionOptions) ([]byte, error) {
serverAddress := options.ServerAddress
if serverAddress == "" {
serverAddress = h.listenOptions.Listen.Build().String()
}
switch options.Format {
case C.SubscriptionTypeRaw:
if options.Application == C.SubscriptionApplicationShadowrocket {
hostString := base64.URLEncoding.EncodeToString([]byte(
h.service.Name() + ":" + h.service.Password() + "@" + M.ParseSocksaddrHostPort(serverAddress, h.listenOptions.ListenPort).String(),
))
shadowrocketURL := &url.URL{
Scheme: "ss",
Host: "$",
Fragment: options.Remarks,
}
requestParams := make(url.Values)
if len(h.network) == 1 && h.network[0] == N.NetworkTCP {
requestParams.Set("uot", "1")
requestParams.Set("udp-over-tcp", "1")
requestParams.Set("udp_over_tcp", "1")
}
if len(requestParams) > 0 {
shadowrocketURL.RawQuery = requestParams.Encode()
}
return []byte(strings.ReplaceAll(shadowrocketURL.String(), "$", hostString)), nil
}
var useBase64Format bool
if options.Application != "" || !strings.HasPrefix(h.service.Name(), "2022-") {
useBase64Format = true
}
sip002URL := &url.URL{
Scheme: "ss",
Host: M.ParseSocksaddrHostPort(serverAddress, h.listenOptions.ListenPort).String(),
Fragment: options.Remarks,
}
var sip002URI string
if !useBase64Format {
sip002URL.User = url.UserPassword(h.service.Name(), h.service.Password())
sip002URI = sip002URL.String()
} else {
sip002URL.User = url.User("$")
sip002URI = strings.ReplaceAll(sip002URL.String(), "$", base64.URLEncoding.EncodeToString([]byte(h.service.Name()+":"+h.service.Password())))
}
return []byte(sip002URI), nil
case C.SubscriptionTypeSIP008:
return json.Marshal(map[string]any{
"id": uuid.NewV5(uuid.Nil, options.Remarks).String(),
"remarks": options.Remarks,
"server": serverAddress,
"server_port": h.listenOptions.ListenPort,
"password": h.service.Password(),
"method": h.service.Name(),
})
default:
return nil, E.New("unknown subscription format ", options.Format)
}
}

View File

@ -1,6 +1,7 @@
package option
type ExperimentalOptions struct {
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
Subscription *SubscriptionOptions `json:"subscription,omitempty"`
}

15
option/subscription.go Normal file
View File

@ -0,0 +1,15 @@
package option
type SubscriptionOptions struct {
Listen string `json:"listen,omitempty"`
ListenPrefix string `json:"listen_prefix,omitempty"`
TLS *InboundTLSOptions `json:"tls,omitempty"`
ServerAddress string `json:"server_address,omitempty"`
Servers []SubscriptionServerOptions `json:"servers,omitempty"`
}
type SubscriptionServerOptions struct {
InboundTag string `json:"inbound_tag,omitempty"`
ServerAddress string `json:"server_address,omitempty"`
Remarks string `json:"remarks,omitempty"`
}

View File

@ -400,10 +400,28 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
return nil
}
func (r *Router) Inbound(tag string) (adapter.Inbound, bool) {
inbound, loaded := r.inboundByTag[tag]
return inbound, loaded
}
func (r *Router) Outbounds() []adapter.Outbound {
return r.outbounds
}
func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
outbound, loaded := r.outboundByTag[tag]
return outbound, loaded
}
func (r *Router) DefaultOutbound(network string) adapter.Outbound {
if network == N.NetworkTCP {
return r.defaultOutboundForConnection
} else {
return r.defaultOutboundForPacketConnection
}
}
func (r *Router) Start() error {
if r.needGeoIPDatabase {
err := r.prepareGeoIPDatabase()
@ -531,19 +549,6 @@ func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
return rule, nil
}
func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
outbound, loaded := r.outboundByTag[tag]
return outbound, loaded
}
func (r *Router) DefaultOutbound(network string) adapter.Outbound {
if network == N.NetworkTCP {
return r.defaultOutboundForConnection
} else {
return r.defaultOutboundForPacketConnection
}
}
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
if metadata.InboundDetour != "" {
if metadata.LastInbound == metadata.InboundDetour {