-
Notifications
You must be signed in to change notification settings - Fork 1
Add UoT support to egress SOCKS5 server #335
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
6281027
Add UoT (UDP-over-TCP) support to egress SOCKS5 server
myleshorton 862e984
Address PR review feedback
myleshorton f9e0e4e
Address second round of PR review feedback
myleshorton 56e1ed1
revert changes to socks5 egress server, move new sing-box egress serv…
noahlevenson 774d2d1
bump version
noahlevenson 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
Some comments aren't visible on the classic Files Changed page.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package common | ||
|
|
||
| // Must be a valid semver | ||
| const Version = "v2.1.6" | ||
| const Version = "v2.1.7" |
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,61 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net" | ||
| "os" | ||
|
|
||
| "github.com/armon/go-socks5" | ||
|
|
||
| "github.com/getlantern/broflake/common" | ||
| "github.com/getlantern/broflake/egress" | ||
| egcmdcommon "github.com/getlantern/broflake/egress/cmd/common" | ||
| ) | ||
|
|
||
| func main() { | ||
| ctx := context.Background() | ||
| port := os.Getenv("PORT") | ||
| if port == "" { | ||
| port = "8000" | ||
| } | ||
|
|
||
| l, err := net.Listen("tcp", fmt.Sprintf(":%v", port)) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| common.Debugf("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@") | ||
| common.Debugf("@ DANGER @") | ||
| common.Debugf("@ DANGER @") | ||
| common.Debugf("@ DANGER @") | ||
| common.Debugf("@ @") | ||
| common.Debugf("@ This standalone egress server does not use secure TLS @") | ||
| common.Debugf("@ at the QUIC layer! @") | ||
| common.Debugf("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n") | ||
|
|
||
| // And here's why it doesn't use secure TLS at the QUIC layer | ||
| tlsConfig := egcmdcommon.GenerateSelfSignedTLSConfig(true) | ||
|
|
||
| ll, err := egress.NewListener(ctx, l, tlsConfig) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
| defer ll.Close() | ||
|
|
||
| conf := &socks5.Config{ | ||
| Dial: UoTDialer(), | ||
| Resolver: &UoTResolver{}, | ||
| } | ||
| proxy, err := socks5.New(conf) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| common.Debugf("Starting SOCKS5 UoT proxy...") | ||
|
|
||
| err = proxy.Serve(ll) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
| } |
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,279 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/binary" | ||
| "fmt" | ||
| "io" | ||
| "net" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/getlantern/broflake/common" | ||
| ) | ||
|
|
||
| const ( | ||
| uotMagicAddress = "sp.v2.udp-over-tcp.arpa" | ||
| uotLegacyMagicAddress = "sp.udp-over-tcp.arpa" | ||
|
|
||
| // SOCKS5 address types (used in UoT v2 request header via SocksaddrSerializer) | ||
| socksAddrTypeIPv4 = 0x01 | ||
| socksAddrTypeFQDN = 0x03 | ||
| socksAddrTypeIPv6 = 0x04 | ||
|
|
||
| maxUDPPayload = 65507 | ||
| ) | ||
|
|
||
| // UoTResolver is a SOCKS5 DNS resolver that passes UoT magic addresses through | ||
| // without resolution, falling back to standard DNS for everything else. | ||
| // go-socks5 resolves FQDNs before calling Dial, so we must intercept here. | ||
| type UoTResolver struct{} | ||
|
|
||
| func (r *UoTResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { | ||
| if isUoTMagicName(name) { | ||
| // Return nil IP so AddrSpec.Address() falls through to the FQDN, | ||
| // allowing the Dial function to match on the magic address. | ||
| return ctx, nil, nil | ||
| } | ||
| ips, err := net.DefaultResolver.LookupIPAddr(ctx, name) | ||
| if err != nil { | ||
| return ctx, nil, err | ||
| } | ||
| if len(ips) == 0 { | ||
| return ctx, nil, fmt.Errorf("no IP addresses found for %s", name) | ||
| } | ||
| return ctx, ips[0].IP, nil | ||
| } | ||
|
|
||
| func isUoTMagicName(name string) bool { | ||
| name = strings.TrimSuffix(name, ".") | ||
| return strings.EqualFold(name, uotMagicAddress) || strings.EqualFold(name, uotLegacyMagicAddress) | ||
| } | ||
|
|
||
| func isUoTAddress(addr string) bool { | ||
| host := addr | ||
| if h, _, err := net.SplitHostPort(addr); err == nil { | ||
| host = h | ||
| } | ||
| return isUoTMagicName(host) | ||
| } | ||
|
|
||
| // tcpPipeConn wraps a net.Conn (from net.Pipe) so that LocalAddr() returns a | ||
| // *net.TCPAddr. go-socks5 does target.LocalAddr().(*net.TCPAddr) after Dial. | ||
| type tcpPipeConn struct { | ||
| net.Conn | ||
| } | ||
|
|
||
| func (c *tcpPipeConn) LocalAddr() net.Addr { | ||
| return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} | ||
| } | ||
|
|
||
| func (c *tcpPipeConn) RemoteAddr() net.Addr { | ||
| return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} | ||
| } | ||
|
|
||
| type uotRequest struct { | ||
| IsConnect bool | ||
| Destination *net.UDPAddr | ||
| } | ||
|
|
||
| // readUoTRequest reads a UoT v2 request header. | ||
| // Wire format: 1 byte IsConnect (bool) + SOCKS5-style address (ATYP + addr + port). | ||
| func readUoTRequest(r io.Reader) (*uotRequest, error) { | ||
| var isConnect byte | ||
| if err := binary.Read(r, binary.BigEndian, &isConnect); err != nil { | ||
| return nil, fmt.Errorf("read IsConnect: %w", err) | ||
| } | ||
|
|
||
| addr, err := readSocksAddr(r) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("read destination: %w", err) | ||
| } | ||
|
|
||
| return &uotRequest{ | ||
| IsConnect: isConnect != 0, | ||
| Destination: addr, | ||
| }, nil | ||
| } | ||
|
|
||
| // readSocksAddr reads a SOCKS5-style address: ATYP + address + 2-byte port (big-endian). | ||
| func readSocksAddr(r io.Reader) (*net.UDPAddr, error) { | ||
| var atyp byte | ||
| if err := binary.Read(r, binary.BigEndian, &atyp); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| var ip net.IP | ||
| var host string | ||
|
|
||
| switch atyp { | ||
| case socksAddrTypeIPv4: | ||
| ip = make(net.IP, 4) | ||
| if _, err := io.ReadFull(r, ip); err != nil { | ||
| return nil, err | ||
| } | ||
| case socksAddrTypeIPv6: | ||
| ip = make(net.IP, 16) | ||
| if _, err := io.ReadFull(r, ip); err != nil { | ||
| return nil, err | ||
| } | ||
| case socksAddrTypeFQDN: | ||
| var length byte | ||
| if err := binary.Read(r, binary.BigEndian, &length); err != nil { | ||
| return nil, err | ||
| } | ||
| domain := make([]byte, length) | ||
| if _, err := io.ReadFull(r, domain); err != nil { | ||
| return nil, err | ||
| } | ||
| host = string(domain) | ||
| default: | ||
| return nil, fmt.Errorf("unknown address type: %d", atyp) | ||
| } | ||
|
|
||
| var port uint16 | ||
| if err := binary.Read(r, binary.BigEndian, &port); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if host != "" { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| defer cancel() | ||
| ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("resolve %s: %w", host, err) | ||
| } | ||
| if len(ips) == 0 { | ||
| return nil, fmt.Errorf("no IP addresses found for %s", host) | ||
| } | ||
| return &net.UDPAddr{IP: ips[0].IP, Port: int(port)}, nil | ||
| } | ||
|
|
||
| return &net.UDPAddr{IP: ip, Port: int(port)}, nil | ||
| } | ||
|
|
||
| // handleUoT handles a UoT v2 connection by relaying framed UDP packets between | ||
| // a TCP stream and a real UDP socket. | ||
| func handleUoT(tcpConn net.Conn) { | ||
| defer tcpConn.Close() | ||
|
|
||
| req, err := readUoTRequest(tcpConn) | ||
| if err != nil { | ||
| common.Debugf("UoT: failed to read request: %v", err) | ||
| return | ||
| } | ||
|
|
||
| common.Debugf("UoT: relay to %v (isConnect=%v)", req.Destination, req.IsConnect) | ||
|
|
||
| if !req.IsConnect { | ||
| common.Debugf("UoT: non-connect mode not supported") | ||
| return | ||
| } | ||
|
|
||
| if req.Destination.IP.IsLoopback() { | ||
| common.Debugf("UoT: refusing to relay to loopback address %v", req.Destination) | ||
| return | ||
| } | ||
|
|
||
| udpConn, err := net.DialUDP("udp", nil, req.Destination) | ||
| if err != nil { | ||
| common.Debugf("UoT: failed to dial UDP %v: %v", req.Destination, err) | ||
| return | ||
| } | ||
| defer udpConn.Close() | ||
myleshorton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| common.Debugf("UoT: connected UDP to %v", req.Destination) | ||
|
|
||
| done := make(chan struct{}, 2) | ||
|
|
||
| // When one direction exits, close both connections to unblock the other goroutine. | ||
| cleanup := func() { | ||
| tcpConn.Close() | ||
| udpConn.Close() | ||
| done <- struct{}{} | ||
| } | ||
|
|
||
| // TCP → UDP: read length-prefixed packets from TCP, send as raw UDP | ||
| go func() { | ||
| defer cleanup() | ||
| buf := make([]byte, maxUDPPayload) | ||
| for { | ||
| var length uint16 | ||
| if err := binary.Read(tcpConn, binary.BigEndian, &length); err != nil { | ||
| common.Debugf("UoT: TCP read length error: %v", err) | ||
| return | ||
| } | ||
| if int(length) > maxUDPPayload { | ||
| common.Debugf("UoT: dropping oversized frame (%d bytes, max %d)", length, maxUDPPayload) | ||
| if _, err := io.CopyN(io.Discard, tcpConn, int64(length)); err != nil { | ||
myleshorton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| common.Debugf("UoT: TCP discard error: %v", err) | ||
| return | ||
| } | ||
| continue | ||
| } | ||
| if _, err := io.ReadFull(tcpConn, buf[:length]); err != nil { | ||
| common.Debugf("UoT: TCP read payload error: %v", err) | ||
| return | ||
| } | ||
| if _, err := udpConn.Write(buf[:length]); err != nil { | ||
| common.Debugf("UoT: UDP write error: %v", err) | ||
| return | ||
| } | ||
| } | ||
| }() | ||
|
|
||
| // UDP → TCP: read raw UDP, write as length-prefixed packets to TCP | ||
| go func() { | ||
| defer cleanup() | ||
| buf := make([]byte, maxUDPPayload) | ||
| for { | ||
| n, err := udpConn.Read(buf) | ||
| if err != nil { | ||
| common.Debugf("UoT: UDP read error: %v", err) | ||
| return | ||
| } | ||
| if n > maxUDPPayload { | ||
| common.Debugf("UoT: dropping oversized UDP packet (%d bytes)", n) | ||
| continue | ||
| } | ||
| if err := binary.Write(tcpConn, binary.BigEndian, uint16(n)); err != nil { | ||
| common.Debugf("UoT: TCP write length error: %v", err) | ||
| return | ||
| } | ||
| if _, err := tcpConn.Write(buf[:n]); err != nil { | ||
| common.Debugf("UoT: TCP write payload error: %v", err) | ||
| return | ||
| } | ||
| } | ||
| }() | ||
|
|
||
| <-done | ||
myleshorton marked this conversation as resolved.
Show resolved
Hide resolved
myleshorton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // UoTDialer returns a SOCKS5-compatible dial function that intercepts UoT magic | ||
| // addresses and handles them as UDP-over-TCP tunnels, while dialing everything | ||
| // else normally. | ||
| func UoTDialer() func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
| return func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
| if isUoTAddress(addr) { | ||
| common.Debugf("UoT: intercepting %v", addr) | ||
| client, server := net.Pipe() | ||
myleshorton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| connDone := make(chan struct{}) | ||
| go func() { | ||
| select { | ||
| case <-ctx.Done(): | ||
| client.Close() | ||
| server.Close() | ||
| case <-connDone: | ||
| } | ||
| }() | ||
| go func() { | ||
| handleUoT(server) | ||
| close(connDone) | ||
| }() | ||
| return &tcpPipeConn{Conn: client}, nil | ||
| } | ||
| var d net.Dialer | ||
| return d.DialContext(ctx, network, addr) | ||
| } | ||
| } | ||
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.