-
Notifications
You must be signed in to change notification settings - Fork 0
Register direct transport for unbounded signaling #375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
e2ae727
Register direct transport for unbounded signaling
myleshorton d053461
Address PR review: lock-free RoundTrip, clone DefaultTransport, fatal…
myleshorton c0b624a
Add package doc explaining bypass proxy's role vs direct transport
myleshorton eb4a015
Merge remote-tracking branch 'origin/main' into afisk/direct-transpor…
myleshorton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package vpn | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "net" | ||
| "net/http" | ||
| "sync" | ||
|
|
||
| "github.com/sagernet/sing-box/adapter" | ||
| M "github.com/sagernet/sing/common/metadata" | ||
| N "github.com/sagernet/sing/common/network" | ||
| "github.com/sagernet/sing/service" | ||
| ) | ||
|
|
||
| // lazyDirectTransport is an http.RoundTripper registered in the sing-box | ||
| // context before the service is created. It defers to an inner RoundTripper | ||
| // that is set via Resolve() after the direct outbound exists. | ||
| // | ||
| // This allows unbounded (which is constructed during NewServiceWithContext) | ||
| // to hold a reference to this transport, while the actual dialer is wired | ||
| // later — before PostStart, when unbounded first uses it. | ||
| type lazyDirectTransport struct { | ||
| mu sync.RWMutex | ||
| inner http.RoundTripper | ||
| resolved bool | ||
| } | ||
|
|
||
| func (t *lazyDirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| t.mu.RLock() | ||
| inner := t.inner | ||
| resolved := t.resolved | ||
| t.mu.RUnlock() | ||
|
|
||
| if !resolved || inner == nil { | ||
| return nil, fmt.Errorf("direct transport not yet resolved") | ||
| } | ||
| return inner.RoundTrip(req) | ||
| } | ||
|
|
||
| // Resolve builds the underlying http.Transport using the direct outbound's | ||
| // DialContext. Must be called after libbox.NewServiceWithContext but before | ||
| // the sing-box service Start (so that it's ready by PostStart when unbounded | ||
| // first makes HTTP calls). | ||
| func (t *lazyDirectTransport) Resolve(ctx context.Context) error { | ||
| t.mu.Lock() | ||
| defer t.mu.Unlock() | ||
|
|
||
| outboundMgr := service.FromContext[adapter.OutboundManager](ctx) | ||
| if outboundMgr == nil { | ||
| return fmt.Errorf("outbound manager not found in context") | ||
| } | ||
|
|
||
| directOutbound, found := outboundMgr.Outbound("direct") | ||
| if !found { | ||
| return fmt.Errorf("direct outbound not found") | ||
| } | ||
|
|
||
| dialer, ok := directOutbound.(N.Dialer) | ||
| if !ok { | ||
| return fmt.Errorf("direct outbound does not implement N.Dialer") | ||
| } | ||
|
|
||
| baseTransport, ok := http.DefaultTransport.(*http.Transport) | ||
| if !ok { | ||
| return fmt.Errorf("default HTTP transport has unexpected type %T", http.DefaultTransport) | ||
| } | ||
| cloned := baseTransport.Clone() | ||
| cloned.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
| return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) | ||
| } | ||
| t.inner = cloned | ||
| t.resolved = true | ||
| slog.Debug("Direct transport resolved using sing-box direct outbound") | ||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package vpn | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestLazyDirectTransport_UnresolvedRoundTrip(t *testing.T) { | ||
| transport := &lazyDirectTransport{} | ||
| req, _ := http.NewRequest("GET", "http://example.com", nil) | ||
| _, err := transport.RoundTrip(req) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "not yet resolved") | ||
| } | ||
|
|
||
| func TestLazyDirectTransport_ResolvedRoundTrip(t *testing.T) { | ||
| ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
| defer ts.Close() | ||
|
|
||
| transport := &lazyDirectTransport{} | ||
|
|
||
| // Manually resolve with a working transport (simulates what Resolve does) | ||
| transport.mu.Lock() | ||
| transport.inner = http.DefaultTransport | ||
| transport.resolved = true | ||
| transport.mu.Unlock() | ||
|
|
||
| req, _ := http.NewRequest("GET", ts.URL, nil) | ||
| resp, err := transport.RoundTrip(req) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, http.StatusOK, resp.StatusCode) | ||
| resp.Body.Close() | ||
| } | ||
|
|
||
| func TestLazyDirectTransport_ResolveNoOutboundManager(t *testing.T) { | ||
| transport := &lazyDirectTransport{} | ||
| err := transport.Resolve(t.Context()) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "outbound manager not found") | ||
| } | ||
|
|
||
| func TestLazyDirectTransport_ConcurrentAccess(t *testing.T) { | ||
| ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
| defer ts.Close() | ||
|
|
||
| transport := &lazyDirectTransport{} | ||
| transport.mu.Lock() | ||
| transport.inner = http.DefaultTransport | ||
| transport.resolved = true | ||
| transport.mu.Unlock() | ||
|
|
||
| // Fire several concurrent requests to verify no data race | ||
| done := make(chan struct{}, 10) | ||
| for i := 0; i < 10; i++ { | ||
| go func() { | ||
| defer func() { done <- struct{}{} }() | ||
| req, _ := http.NewRequest("GET", ts.URL, nil) | ||
| resp, err := transport.RoundTrip(req) | ||
| assert.NoError(t, err) | ||
| if resp != nil { | ||
| resp.Body.Close() | ||
| } | ||
| }() | ||
| } | ||
| for i := 0; i < 10; i++ { | ||
| <-done | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.