17 "github.com/xtaci/kcp-go/v5"
18 "github.com/xtaci/smux"
19 "www.bamsoftware.com/git/champa.git/noise"
20 "www.bamsoftware.com/git/champa.git/turbotunnel"
23 // smux streams will be closed after this much time without receiving data.
24 const idleTimeout
= 2 * time
.Minute
26 // readKeyFromFile reads a key from a named file.
27 func readKeyFromFile(filename
string) ([]byte, error
) {
28 f
, err
:= os
.Open(filename
)
33 return noise
.ReadKey(f
)
36 // noisePacketConn implements the net.PacketConn interface. It acts as an
37 // intermediary between an upper layer and an inner net.PacketConn, decrypting
38 // packets on ReadFrom and encrypting them on WriteTo.
39 type noisePacketConn
struct {
44 // readNoiseMessageOfTypeFrom returns the first complete Noise message whose
45 // msgTime is wantedType, discarding messages of any other msgType.
46 func readNoiseMessageOfTypeFrom(conn net
.PacketConn
, wantedType
byte) ([]byte, net
.Addr
, error
) {
48 msgType
, msg
, addr
, err
:= noise
.ReadMessageFrom(conn
)
50 if err
, ok
:= err
.(net
.Error
); ok
&& err
.Temporary() {
55 if msgType
== wantedType
{
61 // noiseDial performs a Noise handshake over the given net.PacketConn, and
62 // returns a noisePacketConn with a working noise.Session.
63 func noiseDial(conn net
.PacketConn
, addr net
.Addr
, pubkey
[]byte) (*noisePacketConn
, error
) {
64 p
:= []byte{noise
.MsgTypeHandshakeInit
}
65 pre
, p
, err
:= noise
.InitiateHandshake(p
, pubkey
)
69 // TODO: timeout or context
70 _
, err
= conn
.WriteTo(p
, addr
)
75 msg
, _
, err
:= readNoiseMessageOfTypeFrom(conn
, noise
.MsgTypeHandshakeResp
)
80 sess
, err
:= pre
.FinishHandshake(msg
)
85 return &noisePacketConn
{sess
, conn
}, nil
88 // ReadFrom implements the net.PacketConn interface for noisePacketConn.
89 func (c
*noisePacketConn
) ReadFrom(p
[]byte) (int, net
.Addr
, error
) {
90 msg
, addr
, err
:= readNoiseMessageOfTypeFrom(c
.PacketConn
, noise
.MsgTypeTransport
)
94 dec
, err
:= c
.sess
.Decrypt(nil, msg
)
98 return copy(p
, dec
), addr
, nil
101 // WriteTo implements the net.PacketConn interface for noisePacketConn.
102 func (c
*noisePacketConn
) WriteTo(p
[]byte, addr net
.Addr
) (int, error
) {
103 buf
:= []byte{noise
.MsgTypeTransport
}
104 buf
, err
:= c
.sess
.Encrypt(buf
, p
)
108 return c
.PacketConn
.WriteTo(buf
, addr
)
111 func handle(local
*net
.TCPConn
, sess
*smux
.Session
, conv
uint32) error
{
112 stream
, err
:= sess
.OpenStream()
114 return fmt
.Errorf("session %08x opening stream: %v", conv
, err
)
117 log
.Printf("end stream %08x:%d", conv
, stream
.ID())
120 log
.Printf("begin stream %08x:%d", conv
, stream
.ID())
122 var wg sync
.WaitGroup
126 _
, err
:= io
.Copy(stream
, local
)
128 // smux Stream.Write may return io.EOF.
131 if err
!= nil && !errors
.Is(err
, io
.ErrClosedPipe
) {
132 log
.Printf("stream %08x:%d copy stream←local: %v", conv
, stream
.ID(), err
)
139 _
, err
:= io
.Copy(local
, stream
)
141 // smux Stream.WriteTo may return io.EOF.
144 if err
!= nil && !errors
.Is(err
, io
.ErrClosedPipe
) {
145 log
.Printf("stream %08x:%d copy local←stream: %v", conv
, stream
.ID(), err
)
154 func run(serverURL
, cacheURL
*url
.URL
, front
, localAddr
string, pubkey
[]byte) error
{
155 ln
, err
:= net
.Listen("tcp", localAddr
)
161 http
.DefaultTransport
.(*http
.Transport
).MaxConnsPerHost
= 20
163 var poll PollFunc
= func(ctx context
.Context
, p
[]byte) (io
.ReadCloser
, error
) {
164 return exchangeAMP(ctx
, serverURL
, cacheURL
, front
, p
)
166 pconn
:= NewPollingPacketConn(turbotunnel
.DummyAddr
{}, poll
)
169 // Add a Noise layer over the AMP polling to encrypt and authenticate
171 nconn
, err
:= noiseDial(pconn
, turbotunnel
.DummyAddr
{}, pubkey
)
176 // Open a KCP conn over the Noise layer.
177 conn
, err
:= kcp
.NewConn2(turbotunnel
.DummyAddr
{}, nil, 0, 0, nconn
)
179 return fmt
.Errorf("opening KCP conn: %v", err
)
182 log
.Printf("end session %08x", conn
.GetConv())
185 log
.Printf("begin session %08x", conn
.GetConv())
186 // Permit coalescing the payloads of consecutive sends.
187 conn
.SetStreamMode(true)
188 // Disable the dynamic congestion window (limit only by the maximum of
189 // local and remote static windows).
191 0, // default nodelay
192 0, // default interval
194 1, // nc=1 => congestion window off
196 // ACK received data immediately; this is good in our polling model.
197 conn
.SetACKNoDelay(true)
198 conn
.SetWindowSize(1024, 1024) // Default is 32, 32.
199 // TODO: We could optimize a call to conn.SetMtu here, based on a
200 // maximum URL length we want to send (such as the 8000 bytes
201 // recommended at https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1).
202 // The idea is that if we can slightly reduce the MTU from its default
203 // to permit one more packet per request, we should do it.
204 // E.g. 1400*5 = 7000, but 1320*6 = 7920.
206 // Start a smux session on the Noise channel.
207 smuxConfig
:= smux
.DefaultConfig()
208 smuxConfig
.Version
= 2
209 smuxConfig
.KeepAliveTimeout
= idleTimeout
210 smuxConfig
.MaxReceiveBuffer
= 4 * 1024 * 1024 // default is 4 * 1024 * 1024
211 smuxConfig
.MaxStreamBuffer
= 1 * 1024 * 1024 // default is 65536
212 sess
, err
:= smux
.Client(conn
, smuxConfig
)
214 return fmt
.Errorf("opening smux session: %v", err
)
219 local
, err
:= ln
.Accept()
221 if err
, ok
:= err
.(net
.Error
); ok
&& err
.Temporary() {
228 err
:= handle(local
.(*net
.TCPConn
), sess
, conn
.GetConv())
230 log
.Printf("handle: %v", err
)
239 var pubkeyFilename
string
240 var pubkeyString
string
242 flag
.Usage
= func() {
243 fmt
.Fprintf(flag
.CommandLine
.Output(), `Usage:
244 %[1]s -pubkey-file PUBKEYFILE [-cache CACHEURL] [-front DOMAIN] SERVERURL LOCALADDR
247 %[1]s -pubkey-file server.pub -cache https://amp.cache.example/ -front amp.cache.example https://server.example/champa/ 127.0.0.1:7000
252 flag
.StringVar(&cache
, "cache", "", "URL of AMP cache (try https://cdn.ampproject.org/)")
253 flag
.StringVar(&front
, "front", "", "domain to domain-front HTTPS requests with (try www.google.com)")
254 flag
.StringVar(&pubkeyString
, "pubkey", "", fmt
.Sprintf("server public key (%d hex digits)", noise
.KeyLen
*2))
255 flag
.StringVar(&pubkeyFilename
, "pubkey-file", "", "read server public key from file")
258 log
.SetFlags(log
.LstdFlags | log
.LUTC
)
260 if flag
.NArg() != 2 {
264 serverURL
, err
:= url
.Parse(flag
.Arg(0))
266 fmt
.Fprintf(os
.Stderr
, "cannot parse server URL: %v\n", err
)
269 localAddr
:= flag
.Arg(1)
271 var cacheURL
*url
.URL
273 cacheURL
, err
= url
.Parse(cache
)
275 fmt
.Fprintf(os
.Stderr
, "cannot parse AMP cache URL: %v\n", err
)
281 if pubkeyFilename
!= "" && pubkeyString
!= "" {
282 fmt
.Fprintf(os
.Stderr
, "only one of -pubkey and -pubkey-file may be used\n")
284 } else if pubkeyFilename
!= "" {
286 pubkey
, err
= readKeyFromFile(pubkeyFilename
)
288 fmt
.Fprintf(os
.Stderr
, "cannot read pubkey from file: %v\n", err
)
291 } else if pubkeyString
!= "" {
293 pubkey
, err
= noise
.DecodeKey(pubkeyString
)
295 fmt
.Fprintf(os
.Stderr
, "pubkey format error: %v\n", err
)
299 fmt
.Fprintf(os
.Stderr
, "the -pubkey or -pubkey-file option is required\n")
303 err
= run(serverURL
, cacheURL
, front
, localAddr
, pubkey
)