From 7c08f3b8980db2697c6e4b55331135fa80ac1fa4 Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Sun, 7 Dec 2025 13:52:12 -0800 Subject: [PATCH 1/4] basic sing-box outbound achieved --- .../sing-box/protocol/unbounded/inbound.go | 151 ++++++++++++++++++ .../sing-box/protocol/unbounded/outbound.go | 146 +++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 integrations/sing-box/protocol/unbounded/inbound.go create mode 100644 integrations/sing-box/protocol/unbounded/outbound.go diff --git a/integrations/sing-box/protocol/unbounded/inbound.go b/integrations/sing-box/protocol/unbounded/inbound.go new file mode 100644 index 0000000..17c7a38 --- /dev/null +++ b/integrations/sing-box/protocol/unbounded/inbound.go @@ -0,0 +1,151 @@ +package unbounded + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + "net" + + UBEgresslib "github.com/getlantern/broflake/egress" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + // "github.com/sagernet/sing-box/common/uot" + "github.com/sagernet/sing-box/log" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +// WIP usage: edit sing-box/include/registry.go to import and register this protocol + +// TODO: move options types to github.com/sagernet/sing-box/option +type UnboundedInboundOptions struct { + // TODO: what lives here? You should be able to plumb through two configuration options for + // the WebSocket listener from config.json -- "listen" (the IP addr) and "listen_port" (the port). + // This matches the shape of config.json options for the sing-box HTTP inbound... +} + +// TODO: move this to github.com/sagernet/sing-box/constant/proxy.go +const TypeUnbounded = "unbounded" + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[UnboundedInboundOptions](registry, TypeUnbounded, NewInbound) +} + +type Inbound struct { + inbound.Adapter + router adapter.Router // XXX: is this what we want, or an adapter.Router, or...? + logger log.ContextLogger + listener net.Listener // XXX: this is concretely an egress.proxyListener +} + +func NewInbound( + ctx context.Context, + router adapter.Router, + logger log.ContextLogger, + tag string, + options UnboundedInboundOptions, +) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(TypeUnbounded, tag), + router: router, + logger: logger, + } + + // TODO: get the port from UnboundedInboundOptions + l, err := net.Listen("tcp", fmt.Sprintf(":%v", 8000)) + if err != nil { + return nil, err + } + + // TODO: get this from a sing-box proprietary tls.ServerConfig on the Inbound struct, probably + tlsConfig := generateSelfSignedTLSConfig() + + ll, err := UBEgresslib.NewListener(ctx, l, tlsConfig) + if err != nil { + return nil, err + } + + inbound.listener = ll + + // TODO: it seems more sensible to implement this loop in the Start function (below), but it + // seems that sing-box calls Start more than once (?!) at boot + go func() { + for { + // egresslib.proxyListener always returns a nil error, so we ignore it + conn, _ := inbound.listener.Accept() + source := M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() + dest := M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() + + // TODO: is there a better context to use here? + // TODO: we should pass an N.CloseHandlerFunc, but what should it be/do? + inbound.NewConnectionEx(context.Background(), conn, dest, source, nil) + select {} + } + }() + + return inbound, nil +} + +func (i *Inbound) Start(stage adapter.StartStage) error { + // TODO: start stuff, see existing protocol examples + + // TODO: there must be a way to shut this down with Close() + + return nil +} + +func (i *Inbound) Close() error { + // TODO: close everything down, see existing protocol examples + return nil +} + +func (i *Inbound) NewConnectionEx( + ctx context.Context, + conn net.Conn, + source M.Socksaddr, + destination M.Socksaddr, + onClose N.CloseHandlerFunc, +) { + var metadata adapter.InboundContext + metadata.Inbound = i.Tag() + metadata.InboundType = i.Type() + + // TODO: InboundDetour, InboundOptions + + metadata.OriginDestination = M.SocksaddrFromNet(i.listener.Addr()).Unwrap() + metadata.Source = source + metadata.Destination = destination + + i.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +// TODO: delete me +func generateSelfSignedTLSConfig() *tls.Config { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{"broflake"}, + InsecureSkipVerify: true, + } +} diff --git a/integrations/sing-box/protocol/unbounded/outbound.go b/integrations/sing-box/protocol/unbounded/outbound.go new file mode 100644 index 0000000..2914da2 --- /dev/null +++ b/integrations/sing-box/protocol/unbounded/outbound.go @@ -0,0 +1,146 @@ +package unbounded + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "log" + "math/big" + "net" + "os" + + UBClientcore "github.com/getlantern/broflake/clientcore" + UBCommon "github.com/getlantern/broflake/common" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + singlog "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +// WIP usage: edit sing-box/include/registry.go to import and register this protocol + +type logAdapter struct{ + singBoxLogger singlog.ContextLogger +} + +func (l logAdapter) Write(p []byte) (int, error) { + l.singBoxLogger.Info(string(p)) + return len(p), nil +} + +// TODO: move options types to github.com/sagernet/sing-box/option +type UnboundedOutboundOptions struct { + option.DialerOptions + option.ServerOptions +} + +// TODO: move this to github.com/sagernet/sing-box/constant/proxy.go +const TypeUnbounded = "unbounded" + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[UnboundedOutboundOptions](registry, TypeUnbounded, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + broflakeConn *UBClientcore.BroflakeConn + dial UBClientcore.SOCKS5Dialer +} + +func NewOutbound( + ctx context.Context, + router adapter.Router, + logger singlog.ContextLogger, + tag string, + options UnboundedOutboundOptions, +) (adapter.Outbound, error) { + // TODO: move to UnboundedOutboundOptions and set values correctly + bfOpt := UBClientcore.NewDefaultBroflakeOptions() + rtcOpt := UBClientcore.NewDefaultWebRTCOptions() + egOpt := UBClientcore.NewDefaultEgressOptions() + + la := logAdapter{ + singBoxLogger: logger, + } + + UBCommon.SetDebugLogger(log.New(la, "", 0)) + + BFConn, _, err := UBClientcore.NewBroflake(bfOpt, rtcOpt, egOpt) + if err != nil { + return nil, err + } + + // TODO: we need to get rid of generateSelfSignedTLSConfig() and use a proper TLS cert here. It + // should *prbably* be a sing-box tls.ServerConfig, though it's not clear how that interface + // vibes with what the QUIC library expects... alternatively, we could maybe add a tls.Config to + // the outbound options? But unsure how to get that plumbed through end to end... + QUICLayer, err := UBClientcore.NewQUICLayer(BFConn, generateSelfSignedTLSConfig()) + if err != nil { + return nil, err + } + + dialer := UBClientcore.CreateSOCKS5Dialer(QUICLayer) + + o := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions( + TypeUnbounded, + tag, + []string{N.NetworkTCP}, // XXX: Unbounded only supports TCP (not UDP) for now + options.DialerOptions, + ), + logger: logger, + broflakeConn: BFConn, + dial: dialer, + } + + go QUICLayer.ListenAndMaintainQUICConnection() + return o, nil +} + +func (h *Outbound) DialContext( + ctx context.Context, + network string, + destination M.Socksaddr, +) (net.Conn, error) { + // XXX: this is the log pattern for N.NetworkTCP + h.logger.InfoContext(ctx, "outbound connection to ", destination) + + // XXX: network is ignored by Unbounded's SOCKS5 dialer + return h.dial(ctx, network, destination.String()) +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +// TODO: delete me +func generateSelfSignedTLSConfig() *tls.Config { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{"broflake"}, + } +} From 06f39d7d4462a7d80cbab2cdf6e8a287586c9f73 Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Sun, 7 Dec 2025 13:55:09 -0800 Subject: [PATCH 2/4] formatting --- .../sing-box/protocol/unbounded/outbound.go | 212 +++++++++--------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/integrations/sing-box/protocol/unbounded/outbound.go b/integrations/sing-box/protocol/unbounded/outbound.go index 2914da2..673a141 100644 --- a/integrations/sing-box/protocol/unbounded/outbound.go +++ b/integrations/sing-box/protocol/unbounded/outbound.go @@ -1,146 +1,146 @@ package unbounded import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "log" - "math/big" - "net" - "os" - - UBClientcore "github.com/getlantern/broflake/clientcore" - UBCommon "github.com/getlantern/broflake/common" - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/adapter/outbound" - singlog "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "log" + "math/big" + "net" + "os" + + UBClientcore "github.com/getlantern/broflake/clientcore" + UBCommon "github.com/getlantern/broflake/common" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + singlog "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" ) // WIP usage: edit sing-box/include/registry.go to import and register this protocol -type logAdapter struct{ - singBoxLogger singlog.ContextLogger +type logAdapter struct { + singBoxLogger singlog.ContextLogger } func (l logAdapter) Write(p []byte) (int, error) { - l.singBoxLogger.Info(string(p)) - return len(p), nil + l.singBoxLogger.Info(string(p)) + return len(p), nil } // TODO: move options types to github.com/sagernet/sing-box/option type UnboundedOutboundOptions struct { - option.DialerOptions - option.ServerOptions + option.DialerOptions + option.ServerOptions } // TODO: move this to github.com/sagernet/sing-box/constant/proxy.go const TypeUnbounded = "unbounded" func RegisterOutbound(registry *outbound.Registry) { - outbound.Register[UnboundedOutboundOptions](registry, TypeUnbounded, NewOutbound) + outbound.Register[UnboundedOutboundOptions](registry, TypeUnbounded, NewOutbound) } type Outbound struct { - outbound.Adapter - logger logger.ContextLogger - broflakeConn *UBClientcore.BroflakeConn - dial UBClientcore.SOCKS5Dialer + outbound.Adapter + logger logger.ContextLogger + broflakeConn *UBClientcore.BroflakeConn + dial UBClientcore.SOCKS5Dialer } func NewOutbound( - ctx context.Context, - router adapter.Router, - logger singlog.ContextLogger, - tag string, - options UnboundedOutboundOptions, + ctx context.Context, + router adapter.Router, + logger singlog.ContextLogger, + tag string, + options UnboundedOutboundOptions, ) (adapter.Outbound, error) { - // TODO: move to UnboundedOutboundOptions and set values correctly - bfOpt := UBClientcore.NewDefaultBroflakeOptions() - rtcOpt := UBClientcore.NewDefaultWebRTCOptions() - egOpt := UBClientcore.NewDefaultEgressOptions() - - la := logAdapter{ - singBoxLogger: logger, - } - - UBCommon.SetDebugLogger(log.New(la, "", 0)) - - BFConn, _, err := UBClientcore.NewBroflake(bfOpt, rtcOpt, egOpt) - if err != nil { - return nil, err - } - - // TODO: we need to get rid of generateSelfSignedTLSConfig() and use a proper TLS cert here. It - // should *prbably* be a sing-box tls.ServerConfig, though it's not clear how that interface - // vibes with what the QUIC library expects... alternatively, we could maybe add a tls.Config to - // the outbound options? But unsure how to get that plumbed through end to end... - QUICLayer, err := UBClientcore.NewQUICLayer(BFConn, generateSelfSignedTLSConfig()) - if err != nil { - return nil, err - } - - dialer := UBClientcore.CreateSOCKS5Dialer(QUICLayer) - - o := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions( - TypeUnbounded, - tag, - []string{N.NetworkTCP}, // XXX: Unbounded only supports TCP (not UDP) for now - options.DialerOptions, - ), - logger: logger, - broflakeConn: BFConn, - dial: dialer, - } - - go QUICLayer.ListenAndMaintainQUICConnection() - return o, nil + // TODO: move to UnboundedOutboundOptions and set values correctly + bfOpt := UBClientcore.NewDefaultBroflakeOptions() + rtcOpt := UBClientcore.NewDefaultWebRTCOptions() + egOpt := UBClientcore.NewDefaultEgressOptions() + + la := logAdapter{ + singBoxLogger: logger, + } + + UBCommon.SetDebugLogger(log.New(la, "", 0)) + + BFConn, _, err := UBClientcore.NewBroflake(bfOpt, rtcOpt, egOpt) + if err != nil { + return nil, err + } + + // TODO: we need to get rid of generateSelfSignedTLSConfig() and use a proper TLS cert here. It + // should *prbably* be a sing-box tls.ServerConfig, though it's not clear how that interface + // vibes with what the QUIC library expects... alternatively, we could maybe add a tls.Config to + // the outbound options? But unsure how to get that plumbed through end to end... + QUICLayer, err := UBClientcore.NewQUICLayer(BFConn, generateSelfSignedTLSConfig()) + if err != nil { + return nil, err + } + + dialer := UBClientcore.CreateSOCKS5Dialer(QUICLayer) + + o := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions( + TypeUnbounded, + tag, + []string{N.NetworkTCP}, // XXX: Unbounded only supports TCP (not UDP) for now + options.DialerOptions, + ), + logger: logger, + broflakeConn: BFConn, + dial: dialer, + } + + go QUICLayer.ListenAndMaintainQUICConnection() + return o, nil } func (h *Outbound) DialContext( - ctx context.Context, - network string, - destination M.Socksaddr, + ctx context.Context, + network string, + destination M.Socksaddr, ) (net.Conn, error) { - // XXX: this is the log pattern for N.NetworkTCP - h.logger.InfoContext(ctx, "outbound connection to ", destination) + // XXX: this is the log pattern for N.NetworkTCP + h.logger.InfoContext(ctx, "outbound connection to ", destination) - // XXX: network is ignored by Unbounded's SOCKS5 dialer - return h.dial(ctx, network, destination.String()) + // XXX: network is ignored by Unbounded's SOCKS5 dialer + return h.dial(ctx, network, destination.String()) } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return nil, os.ErrInvalid + return nil, os.ErrInvalid } // TODO: delete me func generateSelfSignedTLSConfig() *tls.Config { - key, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - panic(err) - } - - template := x509.Certificate{SerialNumber: big.NewInt(1)} - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) - if err != nil { - panic(err) - } - keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) - certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) - - tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - panic(err) - } - return &tls.Config{ - Certificates: []tls.Certificate{tlsCert}, - NextProtos: []string{"broflake"}, - } + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{"broflake"}, + } } From 08162e9499e08c48e727db86b0bc038c7207b68e Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Sun, 14 Dec 2025 11:41:18 -0800 Subject: [PATCH 3/4] sing-box integration: plumb through configuration options, improve source tree, cleanup --- integrations/sing-box/constant/unbounded.go | 3 + integrations/sing-box/option/unbounded.go | 31 ++++ .../sing-box/protocol/unbounded/inbound.go | 151 ------------------ .../sing-box/protocol/unbounded/outbound.go | 93 +++++++++-- ui/public/widget.wasm | Bin 11753278 -> 11753278 bytes 5 files changed, 110 insertions(+), 168 deletions(-) create mode 100644 integrations/sing-box/constant/unbounded.go create mode 100644 integrations/sing-box/option/unbounded.go delete mode 100644 integrations/sing-box/protocol/unbounded/inbound.go diff --git a/integrations/sing-box/constant/unbounded.go b/integrations/sing-box/constant/unbounded.go new file mode 100644 index 0000000..59c1969 --- /dev/null +++ b/integrations/sing-box/constant/unbounded.go @@ -0,0 +1,3 @@ +package constant + +const TypeUnbounded = "unbounded" diff --git a/integrations/sing-box/option/unbounded.go b/integrations/sing-box/option/unbounded.go new file mode 100644 index 0000000..997904c --- /dev/null +++ b/integrations/sing-box/option/unbounded.go @@ -0,0 +1,31 @@ +package option + +// Note that values which map to time.Duration in Unbounded's options structs are +// represented here as int (which will be converted to seconds). This means you +// can't set a time.Duration of 0, because we can't disambiguate it from an unset +// value. You also can't set any of the other int types to 0. But you probably +// shouldn't be setting any of this stuff to 0! +type UnboundedOutboundOptions struct { + DialerOptions + ServerOptions + // BroflakeOptions + CTableSize int `json:"c_table_size,omitempty"` + PTableSize int `json:"p_table_size,omitempty"` + BusBufferSz int `json:"bus_buffer_sz,omitempty"` + Netstated string `json:"netstated,omitempty"` + // WebRTCOptions + DiscoverySrv string `json:"discovery_srv,omitempty"` + DiscoveryEndpoint string `json:"discovery_endpoint,omitempty"` + GenesisAddr string `json:"genesis_addr,omitempty"` + NATFailTimeout int `json:"nat_fail_timeout,omitempty"` + STUNBatchSize int `json:"stun_batch_size,omitempty"` + Tag string `json:"tag,omitempty"` + Patience int `json:"patience,omitempty"` + ErrorBackoff int `json:"error_backoff,omitempty"` + ConsumerSessionID string `json:"consumer_session_id,omitempty"` + // EgressOptions + EgressAddr string `json:"egress_addr,omitempty"` + EgressEndpoint string `json:"egress_endpoint,omitempty"` + EgressConnectTimeout int `json:"egress_connect_timeout,omitempty"` + EgressErrorBackoff int `json:"egress_error_backoff,omitempty"` +} diff --git a/integrations/sing-box/protocol/unbounded/inbound.go b/integrations/sing-box/protocol/unbounded/inbound.go deleted file mode 100644 index 17c7a38..0000000 --- a/integrations/sing-box/protocol/unbounded/inbound.go +++ /dev/null @@ -1,151 +0,0 @@ -package unbounded - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "fmt" - "math/big" - "net" - - UBEgresslib "github.com/getlantern/broflake/egress" - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/adapter/inbound" - // "github.com/sagernet/sing-box/common/uot" - "github.com/sagernet/sing-box/log" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -// WIP usage: edit sing-box/include/registry.go to import and register this protocol - -// TODO: move options types to github.com/sagernet/sing-box/option -type UnboundedInboundOptions struct { - // TODO: what lives here? You should be able to plumb through two configuration options for - // the WebSocket listener from config.json -- "listen" (the IP addr) and "listen_port" (the port). - // This matches the shape of config.json options for the sing-box HTTP inbound... -} - -// TODO: move this to github.com/sagernet/sing-box/constant/proxy.go -const TypeUnbounded = "unbounded" - -func RegisterInbound(registry *inbound.Registry) { - inbound.Register[UnboundedInboundOptions](registry, TypeUnbounded, NewInbound) -} - -type Inbound struct { - inbound.Adapter - router adapter.Router // XXX: is this what we want, or an adapter.Router, or...? - logger log.ContextLogger - listener net.Listener // XXX: this is concretely an egress.proxyListener -} - -func NewInbound( - ctx context.Context, - router adapter.Router, - logger log.ContextLogger, - tag string, - options UnboundedInboundOptions, -) (adapter.Inbound, error) { - inbound := &Inbound{ - Adapter: inbound.NewAdapter(TypeUnbounded, tag), - router: router, - logger: logger, - } - - // TODO: get the port from UnboundedInboundOptions - l, err := net.Listen("tcp", fmt.Sprintf(":%v", 8000)) - if err != nil { - return nil, err - } - - // TODO: get this from a sing-box proprietary tls.ServerConfig on the Inbound struct, probably - tlsConfig := generateSelfSignedTLSConfig() - - ll, err := UBEgresslib.NewListener(ctx, l, tlsConfig) - if err != nil { - return nil, err - } - - inbound.listener = ll - - // TODO: it seems more sensible to implement this loop in the Start function (below), but it - // seems that sing-box calls Start more than once (?!) at boot - go func() { - for { - // egresslib.proxyListener always returns a nil error, so we ignore it - conn, _ := inbound.listener.Accept() - source := M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() - dest := M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() - - // TODO: is there a better context to use here? - // TODO: we should pass an N.CloseHandlerFunc, but what should it be/do? - inbound.NewConnectionEx(context.Background(), conn, dest, source, nil) - select {} - } - }() - - return inbound, nil -} - -func (i *Inbound) Start(stage adapter.StartStage) error { - // TODO: start stuff, see existing protocol examples - - // TODO: there must be a way to shut this down with Close() - - return nil -} - -func (i *Inbound) Close() error { - // TODO: close everything down, see existing protocol examples - return nil -} - -func (i *Inbound) NewConnectionEx( - ctx context.Context, - conn net.Conn, - source M.Socksaddr, - destination M.Socksaddr, - onClose N.CloseHandlerFunc, -) { - var metadata adapter.InboundContext - metadata.Inbound = i.Tag() - metadata.InboundType = i.Type() - - // TODO: InboundDetour, InboundOptions - - metadata.OriginDestination = M.SocksaddrFromNet(i.listener.Addr()).Unwrap() - metadata.Source = source - metadata.Destination = destination - - i.router.RouteConnectionEx(ctx, conn, metadata, onClose) -} - -// TODO: delete me -func generateSelfSignedTLSConfig() *tls.Config { - key, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - panic(err) - } - - template := x509.Certificate{SerialNumber: big.NewInt(1)} - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) - if err != nil { - panic(err) - } - keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) - certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) - - tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - panic(err) - } - return &tls.Config{ - Certificates: []tls.Certificate{tlsCert}, - NextProtos: []string{"broflake"}, - InsecureSkipVerify: true, - } -} diff --git a/integrations/sing-box/protocol/unbounded/outbound.go b/integrations/sing-box/protocol/unbounded/outbound.go index 673a141..e2c218b 100644 --- a/integrations/sing-box/protocol/unbounded/outbound.go +++ b/integrations/sing-box/protocol/unbounded/outbound.go @@ -11,11 +11,13 @@ import ( "math/big" "net" "os" + "time" UBClientcore "github.com/getlantern/broflake/clientcore" UBCommon "github.com/getlantern/broflake/common" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" + C "github.com/sagernet/sing-box/constant" singlog "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/logger" @@ -34,17 +36,8 @@ func (l logAdapter) Write(p []byte) (int, error) { return len(p), nil } -// TODO: move options types to github.com/sagernet/sing-box/option -type UnboundedOutboundOptions struct { - option.DialerOptions - option.ServerOptions -} - -// TODO: move this to github.com/sagernet/sing-box/constant/proxy.go -const TypeUnbounded = "unbounded" - func RegisterOutbound(registry *outbound.Registry) { - outbound.Register[UnboundedOutboundOptions](registry, TypeUnbounded, NewOutbound) + outbound.Register[option.UnboundedOutboundOptions](registry, C.TypeUnbounded, NewOutbound) } type Outbound struct { @@ -59,12 +52,81 @@ func NewOutbound( router adapter.Router, logger singlog.ContextLogger, tag string, - options UnboundedOutboundOptions, + options option.UnboundedOutboundOptions, ) (adapter.Outbound, error) { - // TODO: move to UnboundedOutboundOptions and set values correctly bfOpt := UBClientcore.NewDefaultBroflakeOptions() + if options.CTableSize != 0 { + bfOpt.CTableSize = options.CTableSize + } + + if options.PTableSize != 0 { + bfOpt.PTableSize = options.PTableSize + } + + if options.BusBufferSz != 0 { + bfOpt.BusBufferSz = options.BusBufferSz + } + + if options.Netstated != "" { + bfOpt.Netstated = options.Netstated + } + rtcOpt := UBClientcore.NewDefaultWebRTCOptions() + if options.DiscoverySrv != "" { + rtcOpt.DiscoverySrv = options.DiscoverySrv + } + + if options.DiscoveryEndpoint != "" { + rtcOpt.Endpoint = options.DiscoveryEndpoint + } + + if options.GenesisAddr != "" { + rtcOpt.GenesisAddr = options.GenesisAddr + } + + if options.NATFailTimeout != 0 { + rtcOpt.NATFailTimeout = time.Duration(options.NATFailTimeout) * time.Second + } + + if options.STUNBatchSize != 0 { + rtcOpt.STUNBatchSize = uint32(options.STUNBatchSize) + } + + if options.Tag != "" { + rtcOpt.Tag = options.Tag + } + + if options.Patience != 0 { + rtcOpt.Patience = time.Duration(options.Patience) * time.Second + } + + if options.ErrorBackoff != 0 { + rtcOpt.ErrorBackoff = time.Duration(options.ErrorBackoff) * time.Second + } + + if options.ConsumerSessionID != "" { + rtcOpt.ConsumerSessionID = options.ConsumerSessionID + } + + // XXX: This sing-box outbound implements a "desktop" type Unbounded peer, and + // desktop peers don't connect to the egress server, so these egress settings + // have no effect. We plumb them through here for the sake of future extensibility. egOpt := UBClientcore.NewDefaultEgressOptions() + if options.EgressAddr != "" { + egOpt.Addr = options.EgressAddr + } + + if options.EgressEndpoint != "" { + egOpt.Endpoint = options.EgressEndpoint + } + + if options.EgressConnectTimeout != 0 { + egOpt.ConnectTimeout = time.Duration(options.EgressConnectTimeout) * time.Second + } + + if options.EgressErrorBackoff != 0 { + egOpt.ErrorBackoff = time.Duration(options.EgressErrorBackoff) * time.Second + } la := logAdapter{ singBoxLogger: logger, @@ -77,10 +139,7 @@ func NewOutbound( return nil, err } - // TODO: we need to get rid of generateSelfSignedTLSConfig() and use a proper TLS cert here. It - // should *prbably* be a sing-box tls.ServerConfig, though it's not clear how that interface - // vibes with what the QUIC library expects... alternatively, we could maybe add a tls.Config to - // the outbound options? But unsure how to get that plumbed through end to end... + // TODO: plumb through a real TLS cert and get rid of the self-signed generator? QUICLayer, err := UBClientcore.NewQUICLayer(BFConn, generateSelfSignedTLSConfig()) if err != nil { return nil, err @@ -90,7 +149,7 @@ func NewOutbound( o := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions( - TypeUnbounded, + C.TypeUnbounded, tag, []string{N.NetworkTCP}, // XXX: Unbounded only supports TCP (not UDP) for now options.DialerOptions, diff --git a/ui/public/widget.wasm b/ui/public/widget.wasm index d9c20e388dd489ecc7f51b1b4de030043f67459f..d7932e961c0ffcb42ecc06af198631b97650c826 100755 GIT binary patch delta 802 zcmYkwM^jS)7>3cY1NPo~MU8|DH#fnG1(c>VQ7kbeM1w&QFrYpb)K~1i_bzs2#$m_W z1uotDPxM>5IGfY^<=3EIb3L;%oS#TFx0Q!$ypniBTezsYbl=9TqNYZ-aO=9w2g0Sv z(%Q0Md&*amRZ>ylCd-`2_KK#4RH~)9x_ECQ>tHg~?&jC-uR0QLtu8O}S~t0c9f*FS zzZf6}ia}zq7$VX{y2ubi#V|2kj1VKmC^1@$5o5(TFr|R2nbIEMM&g{WZK7Qq5l4S;%x^Oe$UBvU4O{u55#jqF)x;z8*qBM%Fg~yEB?PC zUM%R3)9bKg8L56vU<*gP>$ s%`@}dyf81#EA!gCF>lQ~(`DY959Xu!WImfO=BxQ;zW4mB=}!Iq2NCLCx&QzG delta 802 zcmYkwH&>GZ7>40v@4ffljeI461jPBg9BCN{kj`#8@#-j29EcL@`NB7E{DjF-=StGsH|WOUxE? z#9T2?%ohv9La|6J7E8oZu}mx%0kJ})2}iiX6TS$FkVqF9A}k^zQ>+xLM3z`B)`+!Y zomek6h>ap!c zZ=1m!Hz&+VbIP=v(>>k2Xa2NLbUohx&hBGbHkY-H`Dby ze^zuDnX{(ToHOUm1#{6{GMCL2bJcX2Yv#JSVQ!jR=C-+G?wWh%zIk9Cnn&ibd19WL oXXd$iVP2Y7=Cyfa-kNvjz4>51nos7l`C`7BZ#_RNx|{$013krENdN!< From 8923c3bae6c959cf5cc94b6c508dd760b57e9ef8 Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Sun, 14 Dec 2025 11:42:42 -0800 Subject: [PATCH 4/4] bump version --- common/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index f9cf6c4..0a6a42e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,4 +1,4 @@ package common // Must be a valid semver -const Version = "v2.1.3" +const Version = "v2.1.4"