2 udpobfs -- simple point-to-point UDP obfuscation proxy
3 Copyright (C) 2023 Sergey Matveev <stargrave@stargrave.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
37 "go.cypherpunks.ru/udpobfs/v2"
38 "lukechampine.com/blake3"
42 DstAddrUDP *net.UDPAddr
43 DstAddrTCP *net.TCPAddr
46 Peers = make(map[string]chan udpobfs.Buf)
48 Bufs = sync.Pool{New: func() any { return new([udpobfs.BufLen]byte) }}
51 func newPeer(localAddr net.Addr, dataInitial []byte) {
52 logger := slog.With("remote", localAddr.String())
53 logger.Info("connected")
54 conn, err := net.DialTCP("tcp", nil, DstAddrTCP)
56 slog.Warn(err.Error())
60 srcAddr, err := net.ResolveUDPAddr("udp", conn.LocalAddr().String())
64 logger = logger.With("local", srcAddr.String())
65 connTLS := tls.Client(conn, TLSConfig)
66 err = connTLS.Handshake()
68 logger.Error(err.Error())
72 tlsState := connTLS.ConnectionState()
73 logger = logger.With("cn", tlsState.PeerCertificates[0].Subject.CommonName)
74 logger.Info("authenticated")
75 seed, err := tlsState.ExportKeyingMaterial(udpobfs.App, nil, udpobfs.SeedLen)
77 logger.Error(err.Error())
80 connUDP, err := net.ListenUDP("udp", srcAddr)
82 logger.Error(err.Error())
85 cryptoState := udpobfs.NewCryptoState(seed, true)
86 txs := make(chan udpobfs.Buf)
88 Peers[localAddr.String()] = txs
90 var rxPkts, txPkts, rxBytes, txBytes int64
93 txBytes += int64(len(dataInitial))
94 tmp := make([]byte, udpobfs.SeqLen+len(dataInitial))
95 connUDP.WriteTo(cryptoState.Tx(tmp, dataInitial), DstAddrUDP)
97 rxFinished := make(chan struct{})
101 rx := make([]byte, udpobfs.BufLen)
102 tx := make([]byte, udpobfs.BufLen)
105 connUDP.SetReadDeadline(time.Now().Add(2 * udpobfs.LifetimeDuration))
106 n, err = connUDP.Read(rx)
113 if n < udpobfs.SeqLen {
114 logger.Warn("too short")
117 got = cryptoState.Rx(tx[:n], rx[:n])
119 logger.Warn("bad MAC")
124 rxBytes += int64(len(got))
125 LnUDP.WriteTo(got, localAddr)
134 buf := make([]byte, udpobfs.BufLen)
136 ticker := time.NewTicker(udpobfs.PingDuration)
147 if now.Sub(last) > 2*udpobfs.LifetimeDuration {
151 if now.Sub(lastPing) > udpobfs.PingDuration {
152 _, err = connUDP.WriteTo(
153 cryptoState.Tx(buf[:udpobfs.SeqLen], nil), DstAddrUDP)
160 got = cryptoState.Tx(buf[:udpobfs.SeqLen+tx.N], (*tx.Buf)[:tx.N])
162 connUDP.WriteTo(got, DstAddrUDP)
164 txBytes += int64(len(got))
165 lastPing = time.Now()
171 defer connUDP.Close()
172 ticker := time.NewTicker(udpobfs.LifetimeDuration)
174 our := make([]byte, 8)
175 their := make([]byte, 8)
176 key := make([]byte, 32)
177 if _, err = io.ReadFull(rand.Reader, key); err != nil {
180 rnd := blake3.New(32, key).XOF()
185 if _, err = io.ReadFull(rnd, our); err != nil {
188 if _, err = connTLS.Write(our); err != nil {
191 if _, err = io.ReadFull(connTLS, their); err != nil {
194 if !bytes.Equal(our, their) {
195 logger.Error("pong mismatch")
204 logger.Info("finishing",
210 delete(Peers, localAddr.String())
216 bind := flag.String("bind", "[::]:1194", "Address to bind to")
217 dst := flag.String("dst", "[2001:db8::1234]:1194", "Address to connect to")
218 keypairPath := flag.String("keypair", "keypair.pem", "X.509 keypair")
219 caPath := flag.String("ca", "ca.pem", "CA certificate")
220 serverHash := flag.String("hash", "", "Expected server's SPKI SHA256 fingerprint")
221 serverName := flag.String("name", "example.com", "Expected server's hostname")
223 log.SetFlags(log.Lshortfile)
224 slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
226 crtRaw, _, err := udpobfs.CertificateFromFile(*keypairPath)
230 prv, err := udpobfs.PrivateKeyFromFile(*keypairPath)
234 TLSConfig = &tls.Config{
235 MinVersion: tls.VersionTLS13,
236 Certificates: []tls.Certificate{{
237 Certificate: [][]byte{crtRaw},
240 ServerName: *serverName,
242 _, TLSConfig.RootCAs, err = udpobfs.CertPoolFromFile(*caPath)
247 if *serverHash != "" {
248 hshOur, err := hex.DecodeString(*serverHash)
252 TLSConfig.VerifyPeerCertificate = func(
254 verifiedChains [][]*x509.Certificate,
256 spki := verifiedChains[0][0].RawSubjectPublicKeyInfo
257 hshTheir := sha256.Sum256(spki)
258 if !bytes.Equal(hshOur, hshTheir[:]) {
259 return errors.New("server certificate's SPKI hash mismatch")
265 DstAddrUDP = udpobfs.MustResolveUDPAddr(*dst)
266 DstAddrTCP = udpobfs.MustResolveTCPAddr(*dst)
267 LnUDP, err = net.ListenUDP("udp", udpobfs.MustResolveUDPAddr(*bind))
274 var txs chan udpobfs.Buf
275 var buf *[udpobfs.BufLen]byte
277 buf = Bufs.Get().(*[udpobfs.BufLen]byte)
278 n, from, _ = LnUDP.ReadFrom((*buf)[:])
283 txs = Peers[from.String()]
285 txs <- udpobfs.Buf{Buf: buf, N: n}
289 neu := make([]byte, n)
290 copy(neu, (*buf)[:n])
292 go newPeer(from, neu)