This commit is contained in:
2026-08-16 02:50:24 -03:00
parent 6dac260155
commit 5621de243a
8 changed files with 628 additions and 353 deletions
+147 -36
View File
@@ -238,20 +238,25 @@ type vpnClient struct {
ipv4, ipv6 netip.Addr
mtu int
timeout time.Duration
batchDelay time.Duration
reconnectEvery int
upSizer, downSizer *adaptiveSizer
control, upload, download *txnLane
upPackets, downPackets, upBytes, downBytes atomic.Uint64
upBatches, downBatches, localDropped atomic.Uint64
stopped chan struct{}
stopOnce sync.Once
}
func newVPNClient(tun *os.File, addr, token string, v4, v6 netip.Addr, mtu, start, min, max, growAfter, reconnectEvery int, timeout time.Duration, adaptLog bool) (*vpnClient, error) {
func newVPNClient(tun *os.File, addr, token string, v4, v6 netip.Addr, mtu, start, min, max, growAfter, reconnectEvery int, timeout, batchDelay time.Duration, adaptLog bool) (*vpnClient, error) {
sid, err := randomSID()
if err != nil {
return nil, err
}
return &vpnClient{tun: tun, sid: sid, serverAddr: addr, token: token, ipv4: v4, ipv6: v6, mtu: mtu, timeout: timeout, reconnectEvery: reconnectEvery,
if batchDelay < 0 {
batchDelay = 0
}
return &vpnClient{tun: tun, sid: sid, serverAddr: addr, token: token, ipv4: v4, ipv6: v6, mtu: mtu, timeout: timeout, batchDelay: batchDelay, reconnectEvery: reconnectEvery,
upSizer: newSizer("upload", start, min, max, growAfter, adaptLog), downSizer: newSizer("download", start, min, max, growAfter, adaptLog),
control: newTxnLane(addr, timeout, reconnectEvery), upload: newTxnLane(addr, timeout, reconnectEvery), download: newTxnLane(addr, timeout, reconnectEvery), stopped: make(chan struct{})}, nil
}
@@ -296,30 +301,113 @@ func (v *vpnClient) close() {
})
}
func (v *vpnClient) uploadLoop(errs chan<- error) {
buf := make([]byte, 65535)
var seq uint32
func (v *vpnClient) logLocalDrop(reason string) {
n := v.localDropped.Add(1)
// Link-local/control traffic can be noisy. Keep it visible without filling
// the Android live log or making a harmless packet fatal to the VPN.
if n <= 8 || n%256 == 0 {
fmt.Printf("VPN DROP local packet (%s) dropped=%d\n", reason, n)
}
}
func (v *vpnClient) tunReadLoop(out chan<- []byte, errs chan<- error) {
buf := make([]byte, protocol.VPNMaxPacket)
for {
n, err := v.tun.Read(buf)
if err != nil {
errs <- err
return
}
if n < 1 {
if n < 1 || n > protocol.VPNMaxPacket {
continue
}
packet := append([]byte(nil), buf[:n]...)
if n > 65535 {
src, _, err := protocol.PacketAddresses(packet)
if err != nil {
v.logLocalDrop(err.Error())
continue
}
if src != v.ipv4 && src != v.ipv6 {
v.logLocalDrop(fmt.Sprintf("source %s is not assigned VPN address", src))
continue
}
select {
case out <- packet:
case <-v.stopped:
return
}
}
}
func batchWireSize(packets [][]byte) int {
n := 1
for _, p := range packets {
n += 2 + len(p)
}
return n
}
func (v *vpnClient) uploadLoop(in <-chan []byte, errs chan<- error) {
var seq uint32
var carry []byte
for {
var first []byte
if carry != nil {
first, carry = carry, nil
} else {
select {
case first = <-in:
case <-v.stopped:
return
}
}
packets := [][]byte{first}
encodedSize := 1 + 2 + len(first)
timer := time.NewTimer(v.batchDelay)
collect:
for encodedSize < protocol.VPNMaxBatch {
select {
case p := <-in:
need := 2 + len(p)
if encodedSize+need > protocol.VPNMaxBatch {
carry = p
break collect
}
packets = append(packets, p)
encodedSize += need
case <-timer.C:
break collect
case <-v.stopped:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
return
}
}
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
batch, err := protocol.BuildVPNBatch(packets)
if err != nil {
errs <- err
return
}
offset := 0
for offset < n {
for offset < len(batch) {
limit := v.upSizer.Current()
size := n - offset
size := len(batch) - offset
if size > limit {
size = limit
}
req, e := protocol.BuildVPNPush(v.sid, seq, offset, n, packet[offset:offset+size])
req, e := protocol.BuildVPNPush(v.sid, seq, offset, len(batch), batch[offset:offset+size])
if e != nil {
errs <- e
return
@@ -334,16 +422,20 @@ func (v *vpnClient) uploadLoop(errs chan<- error) {
errs <- e
return
}
if rseq != seq || accepted < offset || accepted > n {
if rseq != seq || accepted < offset || accepted > len(batch) {
errs <- errors.New("bad server upload ACK")
return
}
fullRecord := size == limit
v.upSizer.Success(size, fullRecord)
v.upSizer.Success(size, size == limit)
offset = accepted
}
v.upPackets.Add(1)
v.upBytes.Add(uint64(n))
var rawBytes uint64
for _, p := range packets {
rawBytes += uint64(len(p))
}
v.upPackets.Add(uint64(len(packets)))
v.upBytes.Add(rawBytes)
v.upBatches.Add(1)
seq++
}
}
@@ -352,7 +444,7 @@ func (v *vpnClient) downloadLoop(errs chan<- error) {
var want uint32
ack := protocol.VPNNoAck
offset := 0
var packet []byte
var transfer []byte
total := 0
for {
limit := v.downSizer.Current()
@@ -374,42 +466,58 @@ func (v *vpnClient) downloadLoop(errs chan<- error) {
if wait {
continue
}
if seq != want || roff != offset || rtotal < 1 || rtotal > 65535 {
if seq != want || roff != offset || rtotal < 1 || rtotal > protocol.VPNMaxBatch {
errs <- errors.New("bad server download sequence")
return
}
if offset == 0 {
total = rtotal
packet = make([]byte, 0, total)
transfer = make([]byte, 0, total)
} else if rtotal != total {
errs <- errors.New("download packet size changed")
errs <- errors.New("download transfer size changed")
return
}
packet = append(packet, data...)
transfer = append(transfer, data...)
offset += len(data)
v.downSizer.Success(len(data), len(data) == limit)
if offset < total {
continue
}
if offset != total {
errs <- errors.New("download packet overflow")
errs <- errors.New("download transfer overflow")
return
}
n, e := v.tun.Write(packet)
packets, e := protocol.ParseVPNBatch(transfer)
if e != nil {
errs <- e
return
// Compatibility with the first packet-VPN build, which used one raw
// IP packet as each transfer object.
if len(transfer) > 0 && (transfer[0]>>4 == 4 || transfer[0]>>4 == 6) {
packets = [][]byte{transfer}
} else {
errs <- e
return
}
}
if n != len(packet) {
errs <- io.ErrShortWrite
return
var rawBytes uint64
for _, packet := range packets {
n, e := v.tun.Write(packet)
if e != nil {
errs <- e
return
}
if n != len(packet) {
errs <- io.ErrShortWrite
return
}
rawBytes += uint64(n)
}
v.downPackets.Add(1)
v.downBytes.Add(uint64(n))
v.downPackets.Add(uint64(len(packets)))
v.downBytes.Add(rawBytes)
v.downBatches.Add(1)
ack = want
want++
offset = 0
packet = nil
transfer = nil
total = 0
}
}
@@ -419,8 +527,10 @@ func (v *vpnClient) run() error {
return err
}
fmt.Println("VPN READY")
errs := make(chan error, 2)
go v.uploadLoop(errs)
errs := make(chan error, 3)
packets := make(chan []byte, 256)
go v.tunReadLoop(packets, errs)
go v.uploadLoop(packets, errs)
go v.downloadLoop(errs)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
@@ -429,7 +539,7 @@ func (v *vpnClient) run() error {
case err := <-errs:
return err
case <-ticker.C:
fmt.Printf("STATS up_packets=%d down_packets=%d up_bytes=%d down_bytes=%d upload_chunk=%d download_chunk=%d pollers=1\n", v.upPackets.Load(), v.downPackets.Load(), v.upBytes.Load(), v.downBytes.Load(), v.upSizer.Current(), v.downSizer.Current())
fmt.Printf("STATS up_packets=%d down_packets=%d up_batches=%d down_batches=%d up_bytes=%d down_bytes=%d local_dropped=%d upload_chunk=%d download_chunk=%d pollers=1\n", v.upPackets.Load(), v.downPackets.Load(), v.upBatches.Load(), v.downBatches.Load(), v.upBytes.Load(), v.downBytes.Load(), v.localDropped.Load(), v.upSizer.Current(), v.downSizer.Current())
case <-v.stopped:
return nil
}
@@ -445,11 +555,12 @@ func main() {
ipv4Text := flag.String("vpn-ipv4", "10.123.0.2", "client VPN IPv4 address")
ipv6Text := flag.String("vpn-ipv6", "fd7a:4472:6167:6f6e::2", "client VPN IPv6 address")
mtu := flag.Int("vpn-mtu", 1280, "VPN interface MTU")
chunkMax := flag.Int("chunk-max", 65535, "maximum adaptive record bytes")
chunkMax := flag.Int("chunk-max", protocol.VPNMaxFragment, "maximum adaptive record bytes (up to 1 MiB)")
chunkMin := flag.Int("chunk-min", 32, "minimum adaptive record bytes")
chunkStart := flag.Int("chunk-start", 65535, "starting record bytes; app sets this equal to max")
chunkStart := flag.Int("chunk-start", protocol.VPNMaxFragment, "starting record bytes; app sets this equal to max")
growAfter := flag.Int("chunk-grow-after", 64, "full successful records before increasing chunk size")
timeout := flag.Duration("chunk-timeout", 2*time.Second, "framed transaction timeout")
batchDelay := flag.Duration("batch-delay", time.Millisecond, "maximum delay used to combine adjacent TUN packets into one transfer object")
reconnectEvery := flag.Int("chunk-reconnect-every", 32, "reconnect a TCP/53 lane after this many transactions; 0 keeps it open")
adaptLog := flag.Bool("chunk-adapt-log", false, "log adaptive chunk changes")
flag.Parse()
@@ -496,7 +607,7 @@ func main() {
}
}
addr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort))
client, err := newVPNClient(tun, addr, *token, v4, v6, *mtu, *chunkStart, *chunkMin, *chunkMax, *growAfter, *reconnectEvery, *timeout, *adaptLog)
client, err := newVPNClient(tun, addr, *token, v4, v6, *mtu, *chunkStart, *chunkMin, *chunkMax, *growAfter, *reconnectEvery, *timeout, *batchDelay, *adaptLog)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)