Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
15a054c
fix(security): harden trojan proxy against auth, CRLF and resource-ex…
upbeat-backbone-bose Aug 8, 2026
0380784
test: add regression tests for security fixes and tunnel edge cases
upbeat-backbone-bose Aug 8, 2026
c649614
fix(security): validate CRLF terminator on full sniffed and websocket…
upbeat-backbone-bose Aug 8, 2026
7620be4
docs: document rate limiting / DoS hardening
upbeat-backbone-bose Aug 8, 2026
0cd8800
style: gofmt test files added in the Aug-08 security-hardening commits
upbeat-backbone-bose Aug 8, 2026
306efe5
fix(security): reject storage key injection, rate-limit CONNECT flood…
upbeat-backbone-bose Aug 9, 2026
8ec4c95
ci: add gauntlet.sh and mutants.sh for reproducible evidence
upbeat-backbone-bose Aug 9, 2026
69da479
test: surface Dial error in write-error close assertion
upbeat-backbone-bose Aug 9, 2026
f2c6ac6
fix(test): make write-error close test OS-independent
upbeat-backbone-bose Aug 9, 2026
92de5d1
fix(trojan): grace window on half-close instead of immediate peer wake
upbeat-backbone-bose Aug 9, 2026
5a6035e
fix(listener): clear sniff deadline before handing conn off
upbeat-backbone-bose Aug 9, 2026
368e2ad
fix(handler): clear WS header deadline before tunnel; bound failState…
upbeat-backbone-bose Aug 9, 2026
fb8d191
test(handler): pin cleanupLoop NewTicker guard against tiny cooldown
upbeat-backbone-bose Aug 9, 2026
0632278
deps: bump github.com/imgk/memory-go from v0.2.0 to v0.3.0
upbeat-backbone-bose Aug 11, 2026
ea4ccd2
fix(trojan): preserve reader's CRLF error in HandleUDP
upbeat-backbone-bose Aug 11, 2026
65b7a42
fix(trojan): harden HandleUDP address type assertion
upbeat-backbone-bose Aug 11, 2026
1a317e5
fix(trojan): remove (-1,-1,nil) sentinel from allocShortCircuit
upbeat-backbone-bose Aug 11, 2026
09cb1eb
fix(trojan): align HandleUDP non-UDPAddr comment with break+error beh…
upbeat-backbone-bose Aug 11, 2026
f587ba1
fix(app): protect memStorage test helper with mutex
upbeat-backbone-bose Aug 11, 2026
545c582
fix(x): use unsafe.StringData in StringToByteSlice to satisfy vet
upbeat-backbone-bose Aug 11, 2026
78f29da
feat(trojan): add wakeableConn abstraction for half-close grace window
upbeat-backbone-bose Aug 11, 2026
5264c88
docs(rawconn): document *tls.Conn CloseWrite limitation
upbeat-backbone-bose Aug 11, 2026
fd15ea4
fix(trojan): export wakeableConn method and handle gorilla hideTempErr
upbeat-backbone-bose Aug 11, 2026
76a1e2c
docs(trojan): explain why HandleTCP's main goroutine keeps errors.Is
upbeat-backbone-bose Aug 11, 2026
d0636a7
fix(trojan): coalesce HandleUDP teardown timeouts back to nil
upbeat-backbone-bose Aug 11, 2026
035b7f1
test(trojan): add client-to-upstream flow integration test
upbeat-backbone-bose Aug 11, 2026
873f130
fix(trojan): restore e44c4a3 reader-release on hard-error break + com…
upbeat-backbone-bose Aug 12, 2026
ec9d173
chore(trojan): PR review feedback — F1-adjacent hardening
upbeat-backbone-bose Aug 11, 2026
9bf0717
fix(trojan): add wakeableConn compile-time guards + assert-nil tests …
upbeat-backbone-bose Aug 12, 2026
39ff1ae
docs(trojan): clean up wakeable.go doc comments
upbeat-backbone-bose Aug 12, 2026
fb7cc2e
style: gofmt fixes and condense verbose comments
upbeat-backbone-bose Aug 12, 2026
c263a18
chore(deps): bump github.com/google/cel-go
dependabot[bot] Aug 19, 2026
0d50ab3
Merge pull request #4 from upbeat-backbone-bose/dependabot/go_modules…
upbeat-backbone-bose Sep 20, 2026
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,39 @@ $ xcaddy build --with github.com/imgk/caddy-trojan
curl -X POST -H "Content-Type: application/json" -d '{"password": "test1234"}' http://localhost:2019/trojan/users/add
```

## Rate Limiting / DoS Hardening

Password validation applies a uniform 250ms delay on every connection (hit and
miss alike, to keep validation timing-constant). This means an unauthenticated
peer can hold a server goroutine + file descriptor for ~250ms per connection by
sending 58 bytes after the TLS handshake. Caddy itself has no built-in
per-IP connection limiting, so add one at the edge:

1. Bound connection lifetime in the server block so stalled peers are dropped:
```
servers :443 {
listener_wrappers {
trojan
}
read_timeout 30s
write_timeout 30s
idle_timeout 1m
}
```
2. For per-IP connection concurrency limits, build caddy with a limiting
plugin, e.g. [mholt/caddy-limit](https://github.com/mholt/caddy-limit):
```
xcaddy build --with github.com/mholt/caddy-limit
```
```
servers :443 {
...
conn_limit 20
}
```
3. As a last line of defense, use a firewall / fail2ban at the host level to
cap new connections per source IP on port 443.

## Docker

```
Expand Down
44 changes: 36 additions & 8 deletions app/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net"
"net/http"
"strings"
"sync"

"golang.org/x/net/proxy"
Expand All @@ -17,6 +18,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig"

"github.com/imgk/caddy-trojan/pkgs/domaintree"
"github.com/imgk/caddy-trojan/pkgs/rawconn"
"github.com/imgk/caddy-trojan/pkgs/trojan"
"github.com/imgk/caddy-trojan/pkgs/x"
)
Expand Down Expand Up @@ -145,7 +147,7 @@ func (p *EnvProxy) Provision(ctx caddy.Context) error {
if p.ProxyRaw != nil {
mod, err := ctx.LoadModule(p, "ProxyRaw")
if err != nil {
return nil
return err
}
p.proxy = mod.(Proxy)
p.dialer = proxy.FromEnvironmentUsing(p.proxy)
Expand Down Expand Up @@ -210,7 +212,7 @@ func (p *SocksProxy) Provision(ctx caddy.Context) error {
if p.ProxyRaw != nil {
mod, err := ctx.LoadModule(p, "ProxyRaw")
if err != nil {
return nil
return err
}
p.proxy = mod.(Proxy)
} else {
Expand Down Expand Up @@ -291,7 +293,7 @@ func (p *HttpProxy) Provision(ctx caddy.Context) error {
if p.ProxyRaw != nil {
mod, err := ctx.LoadModule(p, "ProxyRaw")
if err != nil {
return nil
return err
}
p.proxy = mod.(Proxy)
} else {
Expand All @@ -310,15 +312,30 @@ func (p *HttpProxy) Close() error {
return p.proxy.Close()
}

// validateProxyTarget rejects target addresses that could break out of the
// HTTP CONNECT request line (CR/LF header injection) or smuggle bytes into
// the upstream proxy connection.
func validateProxyTarget(addr string) error {
if strings.ContainsAny(addr, "\r\n\x00 \t") {
return errors.New("invalid target address: control characters are not allowed")
}
return nil
}

func (p *HttpProxy) Dial(network, addr string) (net.Conn, error) {
if err := validateProxyTarget(addr); err != nil {
return nil, err
}

conn, err := p.proxy.Dial("tcp", p.Server)
if err != nil {
return nil, err
}

req, err := http.NewRequest(http.MethodConnect, p.tcpURL, nil)
if err != nil {
return conn, fmt.Errorf("request error: %w", err)
_ = conn.Close()
return nil, fmt.Errorf("request error: %w", err)
}
req.URL.Opaque = addr
if p.basicAuth != "" {
Expand All @@ -327,15 +344,26 @@ func (p *HttpProxy) Dial(network, addr string) (net.Conn, error) {

err = req.WriteProxy(conn)
if err != nil {
return conn, fmt.Errorf("write request error: %w", err)
_ = conn.Close()
return nil, fmt.Errorf("write request error: %w", err)
}

resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, nil)
if err != nil {
return conn, fmt.Errorf("read response error: %w", err)
_ = conn.Close()
return nil, fmt.Errorf("read response error: %w", err)
}
if resp.StatusCode != http.StatusOK {
return conn, fmt.Errorf("server status code error: %v", resp.StatusCode)
_ = conn.Close()
return nil, fmt.Errorf("server status code error: %v", resp.StatusCode)
}
// bufio may have read past the response head into the tunnel payload;
// replay those bytes so the first tunneled packet is not lost.
if n := br.Buffered(); n > 0 {
if b, err := br.Peek(n); err == nil {
return rawconn.NewConn(conn, b), nil
}
}
return conn, nil
}
Expand Down
Loading
Loading