Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions infra/conf/transport_internet.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ type SplitHTTPConfig struct {
SessionKey string `json:"sessionKey"`
SeqPlacement string `json:"seqPlacement"`
SeqKey string `json:"seqKey"`
PathPool []string `json:"pathPool"`
UplinkDataPlacement string `json:"uplinkDataPlacement"`
UplinkDataKey string `json:"uplinkDataKey"`
UplinkChunkSize Int32Range `json:"uplinkChunkSize"`
Expand Down Expand Up @@ -406,6 +407,7 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
SeqPlacement: c.SeqPlacement,
SessionKey: c.SessionKey,
SeqKey: c.SeqKey,
PathPool: c.PathPool,
UplinkDataPlacement: c.UplinkDataPlacement,
UplinkDataKey: c.UplinkDataKey,
UplinkChunkSize: newRangeConfig(c.UplinkChunkSize),
Expand Down
6 changes: 6 additions & 0 deletions transport/internet/splithttp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ func (c *Config) FillStreamRequest(request *http.Request, sessionId string, seqS

c.ApplyXPaddingToRequest(request, config)
c.ApplyMetaToRequest(request, sessionId, "")
if c.pathMetaIsOffPath() {
c.DecorateRequestPath(request)
}

if request.Body != nil && !c.NoGRPCHeader { // stream-up/one
request.Header.Set("Content-Type", "application/grpc")
Expand Down Expand Up @@ -366,6 +369,9 @@ func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqS

c.ApplyXPaddingToRequest(request, config)
c.ApplyMetaToRequest(request, sessionId, seqStr)
if c.pathMetaIsOffPath() {
c.DecorateRequestPath(request)
}

return nil
}
Expand Down
13 changes: 11 additions & 2 deletions transport/internet/splithttp/config.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions transport/internet/splithttp/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ message Config {
string uplinkDataKey = 25;
RangeConfig uplinkChunkSize = 26;
int32 serverMaxHeaderBytes = 27;
repeated string pathPool = 28;
}
62 changes: 62 additions & 0 deletions transport/internet/splithttp/obfs_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package splithttp

import (
"net/http"
"strconv"
"strings"
)

// Per-request path randomization (operator supplied).
//
// When the session and seq are carried off-path (cookie / header / query), the
// URL path is free decoration: the server only checks
// strings.HasPrefix(reqPath, configPath) and ignores everything after the
// configured base path. If the operator sets pathPool, each request appends a
// randomly chosen entry to the base path, so the transport stops sending every
// request to one fixed path. A "*" inside an entry is replaced with a random
// decimal number, so a short list can cover id / cursor style paths
// (e.g. "items/*", "users/*/feed").
//
// No pool is shipped by default. A fixed list baked into the binary would just
// become a new shared signature; the operator picks entries that match the
// traffic they want to blend into.

func randIntn(n int) int {
if n <= 0 {
return 0
}
s, ok := randStringFromCharset(6, "0123456789")
if !ok {
return 0
}
v, err := strconv.Atoi(s)
if err != nil {
return 0
}
return v % n
}

// pathMetaIsOffPath reports whether both session and seq are carried somewhere
// other than the path, which leaves the path free to decorate.
func (c *Config) pathMetaIsOffPath() bool {
return c.GetNormalizedSessionPlacement() != PlacementPath &&
c.GetNormalizedSeqPlacement() != PlacementPath
}

// DecorateRequestPath appends a random entry from PathPool to req.URL.Path.
// It is a no-op when no pool is configured.
func (c *Config) DecorateRequestPath(req *http.Request) {
if len(c.PathPool) == 0 {
return
}
seg := c.PathPool[randIntn(len(c.PathPool))]
for strings.Contains(seg, "*") {
seg = strings.Replace(seg, "*", strconv.Itoa(1000+randIntn(900000)), 1)
}
seg = strings.TrimPrefix(seg, "/")
base := req.URL.Path
if !strings.HasSuffix(base, "/") {
base += "/"
}
req.URL.Path = base + seg
}
34 changes: 34 additions & 0 deletions transport/internet/splithttp/obfs_path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package splithttp

import (
"net/http/httptest"
"strings"
"testing"
)

func TestDecorateRequestPath(t *testing.T) {
// No pool: the path is left untouched.
c := &Config{}
req := httptest.NewRequest("GET", "https://example.com/api/v1/", nil)
c.DecorateRequestPath(req)
if req.URL.Path != "/api/v1/" {
t.Fatalf("empty pool changed the path: %q", req.URL.Path)
}

// With a pool: append one entry, keep the base prefix, expand "*".
c = &Config{PathPool: []string{"timeline", "items/*"}}
for i := 0; i < 50; i++ {
req := httptest.NewRequest("GET", "https://example.com/api/v1/", nil)
c.DecorateRequestPath(req)
rest, ok := strings.CutPrefix(req.URL.Path, "/api/v1/")
if !ok {
t.Fatalf("lost the base prefix: %q", req.URL.Path)
}
if strings.Contains(rest, "*") {
t.Fatalf("star was not expanded: %q", rest)
}
if rest != "timeline" && !strings.HasPrefix(rest, "items/") {
t.Fatalf("unexpected segment: %q", rest)
}
}
}