1 // udpobfs -- simple point-to-point UDP obfuscation proxy
2 // Copyright (C) 2023-2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
35 "go.cypherpunks.ru/udpobfs/v2"
36 "lukechampine.com/blake3"
40 DstAddrUDP *net.UDPAddr
41 DstAddrTCP *net.TCPAddr
44 Peers = make(map[string]chan udpobfs.Buf)
46 Bufs = sync.Pool{New: func() any { return new([udpobfs.BufLen]byte) }}
49 func newPeer(localAddr net.Addr, dataInitial []byte) {
50 logger := slog.With("remote", localAddr.String())
51 logger.Info("connected")
52 conn, err := net.DialTCP("tcp", nil, DstAddrTCP)
54 slog.Warn(err.Error())
58 srcAddr, err := net.ResolveUDPAddr("udp", conn.LocalAddr().String())
62 logger = logger.With("local", srcAddr.String())
63 connTLS := tls.Client(conn, TLSConfig)
64 err = connTLS.Handshake()
66 logger.Error(err.Error())
70 tlsState := connTLS.ConnectionState()
71 logger = logger.With("cn", tlsState.PeerCertificates[0].Subject.CommonName)
72 logger.Info("authenticated")
73 seed, err := tlsState.ExportKeyingMaterial(udpobfs.App, nil, udpobfs.SeedLen)
75 logger.Error(err.Error())
78 connUDP, err := net.ListenUDP("udp", srcAddr)
80 logger.Error(err.Error())
83 cryptoState := udpobfs.NewCryptoState(seed, true)
84 txs := make(chan udpobfs.Buf)
86 Peers[localAddr.String()] = txs
88 var rxPkts, txPkts, rxBytes, txBytes int64
91 txBytes += int64(len(dataInitial))
92 tmp := make([]byte, udpobfs.SeqLen+len(dataInitial))
93 connUDP.WriteTo(cryptoState.Tx(tmp, dataInitial), DstAddrUDP)
95 rxFinished := make(chan struct{})
99 rx := make([]byte, udpobfs.BufLen)
100 tx := make([]byte, udpobfs.BufLen)
103 connUDP.SetReadDeadline(time.Now().Add(2 * udpobfs.LifetimeDuration))
104 n, err = connUDP.Read(rx)
111 if n < udpobfs.SeqLen {
112 logger.Warn("too short")
115 got = cryptoState.Rx(tx[:n], rx[:n])
117 logger.Warn("bad MAC")
122 rxBytes += int64(len(got))
123 LnUDP.WriteTo(got, localAddr)
132 buf := make([]byte, udpobfs.BufLen)
134 ticker := time.NewTicker(udpobfs.PingDuration)
145 if now.Sub(last) > 2*udpobfs.LifetimeDuration {
149 if now.Sub(lastPing) > udpobfs.PingDuration {
150 _, err = connUDP.WriteTo(
151 cryptoState.Tx(buf[:udpobfs.SeqLen], nil), DstAddrUDP)
158 got = cryptoState.Tx(buf[:udpobfs.SeqLen+tx.N], (*tx.Buf)[:tx.N])
160 connUDP.WriteTo(got, DstAddrUDP)
162 txBytes += int64(len(got))
163 lastPing = time.Now()
169 defer connUDP.Close()
170 ticker := time.NewTicker(udpobfs.LifetimeDuration)
172 our := make([]byte, 8)
173 their := make([]byte, 8)
174 key := make([]byte, 32)
175 if _, err = io.ReadFull(rand.Reader, key); err != nil {
178 rnd := blake3.New(32, key).XOF()
183 if _, err = io.ReadFull(rnd, our); err != nil {
186 if _, err = connTLS.Write(our); err != nil {
189 if _, err = io.ReadFull(connTLS, their); err != nil {
192 if !bytes.Equal(our, their) {
193 logger.Error("pong mismatch")
202 logger.Info("finishing",
208 delete(Peers, localAddr.String())
214 bind := flag.String("bind", "[::]:1194", "Address to bind to")
215 dst := flag.String("dst", "[2001:db8::1234]:1194", "Address to connect to")
216 keypairPath := flag.String("keypair", "keypair.pem", "X.509 keypair")
217 caPath := flag.String("ca", "ca.pem", "CA certificate")
218 serverHash := flag.String("hash", "", "Expected server's SPKI SHA256 fingerprint")
219 serverName := flag.String("name", "example.com", "Expected server's hostname")
221 log.SetFlags(log.Lshortfile)
222 slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
224 crtRaw, _, err := udpobfs.CertificateFromFile(*keypairPath)
228 prv, err := udpobfs.PrivateKeyFromFile(*keypairPath)
232 TLSConfig = &tls.Config{
233 MinVersion: tls.VersionTLS13,
234 Certificates: []tls.Certificate{{
235 Certificate: [][]byte{crtRaw},
238 ServerName: *serverName,
240 _, TLSConfig.RootCAs, err = udpobfs.CertPoolFromFile(*caPath)
245 if *serverHash != "" {
246 hshOur, err := hex.DecodeString(*serverHash)
250 TLSConfig.VerifyPeerCertificate = func(
252 verifiedChains [][]*x509.Certificate,
254 spki := verifiedChains[0][0].RawSubjectPublicKeyInfo
255 hshTheir := sha256.Sum256(spki)
256 if !bytes.Equal(hshOur, hshTheir[:]) {
257 return errors.New("server certificate's SPKI hash mismatch")
263 DstAddrUDP = udpobfs.MustResolveUDPAddr(*dst)
264 DstAddrTCP = udpobfs.MustResolveTCPAddr(*dst)
265 LnUDP, err = net.ListenUDP("udp", udpobfs.MustResolveUDPAddr(*bind))
272 var txs chan udpobfs.Buf
273 var buf *[udpobfs.BufLen]byte
275 buf = Bufs.Get().(*[udpobfs.BufLen]byte)
276 n, from, _ = LnUDP.ReadFrom((*buf)[:])
281 txs = Peers[from.String()]
283 txs <- udpobfs.Buf{Buf: buf, N: n}
287 neu := make([]byte, n)
288 copy(neu, (*buf)[:n])
290 go newPeer(from, neu)