Skip to content
Merged
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 main/distro/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ import (
_ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation"
_ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/server"

_ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorenrollment/clicommand"

// Transport headers
_ "github.com/v2fly/v2ray-core/v5/transport/internet/headers/http"
_ "github.com/v2fly/v2ray-core/v5/transport/internet/headers/noop"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package clicommand

import (
"flag"
"io"
"os"

"google.golang.org/protobuf/encoding/protojson"
anypb "google.golang.org/protobuf/types/known/anypb"

"github.com/v2fly/v2ray-core/v5/main/commands/all/engineering"
"github.com/v2fly/v2ray-core/v5/main/commands/base"
mirrorenrollment "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorenrollment"
)

var (
inputPath *string
outputPath *string
mode *string
)

var cmdEnrollmentLink = &base.Command{
UsageLine: "{{.Exec}} engineering tlsmirror-enrollment-link",
Flag: func() flag.FlagSet {
fs := flag.NewFlagSet("tlsmirror-enrollment-link", flag.ExitOnError)
inputPath = fs.String("c", "", "input file path (optional, defaults to stdin)")
outputPath = fs.String("o", "", "output file path (default stdout)")
mode = fs.String("mode", "link", "conversion mode: 'link' to convert JSON -> link, 'json' to convert link -> JSON")
return *fs
}(),
Run: func(cmd *base.Command, args []string) {
if err := cmd.Flag.Parse(args); err != nil {
base.Fatalf("failed to parse flags: %v", err)
}

var content []byte
var err error
if *inputPath == "" {
// Read from stdin when -c is omitted.
content, err = io.ReadAll(os.Stdin)
if err != nil {
base.Fatalf("failed to read from stdin: %v", err)
}
} else {
fd, err := os.Open(*inputPath)
if err != nil {
base.Fatalf("failed to open input file %q: %v", *inputPath, err)
}
defer fd.Close()

content, err = io.ReadAll(fd)
if err != nil {
base.Fatalf("failed to read input file %q: %v", *inputPath, err)
}
}

var outBytes []byte
switch *mode {
case "link":
// Expect protobuf JSON for google.protobuf.Any, convert to a data URL link.
var any anypb.Any
if err := protojson.Unmarshal(content, &any); err != nil {
base.Fatalf("failed to unmarshal JSON into google.protobuf.Any: %v", err)
}
link, err := mirrorenrollment.LinkFromAny(&any)
if err != nil {
base.Fatalf("failed to create link from Any: %v", err)
}
outBytes = []byte(link)

case "json":
// Expect link (data URL or other supported forms), convert to protobuf JSON.
link := string(content)
any, err := mirrorenrollment.AnyFromLink(link)
if err != nil {
base.Fatalf("failed to parse link into Any: %v", err)
}
b, err := protojson.Marshal(any)
if err != nil {
base.Fatalf("failed to marshal Any to JSON: %v", err)
}
outBytes = b

default:
base.Fatalf("unknown mode: %s", *mode)
}

if *outputPath == "" {
if _, err := os.Stdout.Write(outBytes); err != nil {
base.Fatalf("failed to write output to stdout: %v", err)
}
return
}

if err := os.WriteFile(*outputPath, outBytes, 0o644); err != nil {
base.Fatalf("failed to write output file %q: %v", *outputPath, err)
}
},
}

func init() {
engineering.AddCommand(cmdEnrollmentLink)
}
14 changes: 14 additions & 0 deletions transport/internet/tlsmirror/mirrorenrollment/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ func NewEnrollmentConfirmationClient(
return nil, newError("config cannot be nil")
}

for _, handler := range config.BootstrapEgressUrl {
if handler == "" {
return nil, newError("bootstrap ingress URL cannot be empty")
}
anyPb, err := AnyFromLink(handler)
if err != nil {
return nil, newError("invalid bootstrap ingress URL").Base(err).AtError()
}
if anyPb == nil {
return nil, newError("bootstrap ingress URL did not produce valid Any")
}
config.BootstrapEgressConfig = append(config.BootstrapEgressConfig, anyPb)
}

ecc := &EnrollmentConfirmationClient{
ctx: ctx,
config: config,
Expand Down
94 changes: 94 additions & 0 deletions transport/internet/tlsmirror/mirrorenrollment/enrollmentlink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package mirrorenrollment

import (
"encoding/base64"
"fmt"
"strings"

"google.golang.org/protobuf/proto"
anypb "google.golang.org/protobuf/types/known/anypb"
)

const (
// MIME type used for enrollment data URLs.
enrollmentDataMIME = "application/vnd.v2ray.tlsmirror-enrollment"
)

// LinkFromAny converts a protobuf Any into a data URL string.
// The produced link has form:
// data:application/vnd.v2ray.tlsmirror-enrollment;base64,<base64(payload)>
// where payload is the marshaled Any message encoded with standard base64 (with padding).
func LinkFromAny(a *anypb.Any) (string, error) {
if a == nil {
return "", newError("nil Any")
}
b, err := proto.Marshal(a)
if err != nil {
return "", newError("failed to marshal Any").Base(err)
}
enc := base64.StdEncoding.EncodeToString(b)
dataURL := "data:" + enrollmentDataMIME + ";base64," + enc
return dataURL, nil
}

// AnyFromLink converts a string link (now primarily a data URL) back to *anypb.Any.
// Accepted formats (strict): only data URLs matching the exact MIME type and base64 encoding.
func AnyFromLink(link string) (*anypb.Any, error) {
if link == "" {
return nil, newError("empty link")
}

// Must be a data URL.
if !strings.HasPrefix(link, "data:") {
return nil, newError("input must be a data URL")
}

// Parse and split header and payload at the first comma.
comma := strings.Index(link, ",")
if comma == -1 || comma+1 >= len(link) {
return nil, newError("invalid data URL: missing payload")
}

meta := link[len("data:"):comma]
payload := link[comma+1:]

// Meta should be like "<mime-type>;base64" possibly with additional params.
metaParts := strings.Split(meta, ";")
if len(metaParts) < 2 {
return nil, newError("invalid data URL metadata")
}

// First part must match our expected MIME.
if metaParts[0] != enrollmentDataMIME {
return nil, newError("unexpected MIME type in data URL: " + metaParts[0])
}

// Ensure "base64" is present in parameters.
isBase64 := false
for _, p := range metaParts[1:] {
if p == "base64" {
isBase64 = true
break
}
}
if !isBase64 {
return nil, newError("data URL must be base64 encoded")
}

// No URL parsing/decoding of payload: payload is raw base64.
raw, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return nil, newError("failed to base64-decode data URL payload").Base(err)
}

var any anypb.Any
if err := proto.Unmarshal(raw, &any); err != nil {
return nil, newError("failed to unmarshal Any from decoded payload").Base(err)
}
return &any, nil
}

// helper to format an error when newError is not available at call site
func fmtErr(format string, a ...interface{}) error {
return fmt.Errorf(format, a...)
}
Loading
Loading