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)
47 Bufs = sync.Pool{New: func() any { return new([udpobfs.BufLen]byte) }}
50 func newPeer(localAddr net.Addr, dataInitial []byte) {
51 logger := slog.With("remote", localAddr.String())
52 logger.Info("connected")
53 conn, err := net.DialTCP("tcp", nil, DstAddrTCP)
55 slog.Warn(err.Error())
59 srcAddr, err := net.ResolveUDPAddr("udp", conn.LocalAddr().String())
63 logger = logger.With("local", srcAddr.String())
64 connTLS := tls.Client(conn, TLSConfig)
65 err = connTLS.Handshake()
67 logger.Error(err.Error())
71 tlsState := connTLS.ConnectionState()
72 logger = logger.With("cn", tlsState.PeerCertificates[0].Subject.CommonName)
73 logger.Info("authenticated")
74 seed, err := tlsState.ExportKeyingMaterial(udpobfs.App, nil, udpobfs.SeedLen)
76 logger.Error(err.Error())
79 connUDP, err := net.ListenUDP("udp", srcAddr)
81 logger.Error(err.Error())
84 cryptoState := udpobfs.NewCryptoState(seed, true)
85 txs := make(chan udpobfs.Buf)
86 rxFinished := make(chan struct{})
87 var rxPkts, txPkts, rxBytes, txBytes int64
91 rx := make([]byte, udpobfs.BufLen)
92 tx := make([]byte, udpobfs.BufLen)
95 connUDP.SetReadDeadline(time.Now().Add(2 * udpobfs.LifetimeDuration))
96 n, err = connUDP.Read(rx)
103 if n < udpobfs.SeqLen {
104 logger.Warn("too short")
107 got = cryptoState.Rx(tx[:n], rx[:n])
109 logger.Warn("bad MAC")
114 rxBytes += int64(len(got))
115 LnUDP.WriteTo(got, localAddr)
124 buf := make([]byte, udpobfs.BufLen)
126 ticker := time.NewTicker(udpobfs.PingDuration)
136 if now.Sub(last) > 2*udpobfs.LifetimeDuration {
140 if now.Sub(lastPing) > udpobfs.PingDuration {
141 _, err = connUDP.WriteTo(
142 cryptoState.Tx(buf[:udpobfs.SeqLen], nil), DstAddrUDP)
149 got = cryptoState.Tx(buf[:udpobfs.SeqLen+tx.N], (*tx.Buf)[:tx.N])
151 connUDP.WriteTo(got, DstAddrUDP)
153 txBytes += int64(len(got))
154 lastPing = time.Now()
159 Peers[localAddr.String()] = txs
162 txBytes += int64(len(dataInitial))
163 tmp := make([]byte, udpobfs.SeqLen+len(dataInitial))
164 connUDP.WriteTo(cryptoState.Tx(tmp, dataInitial), DstAddrUDP)
167 defer connUDP.Close()
168 ticker := time.NewTicker(udpobfs.LifetimeDuration)
170 our := make([]byte, 8)
171 their := make([]byte, 8)
172 key := make([]byte, 32)
173 if _, err = io.ReadFull(rand.Reader, key); err != nil {
176 rnd := blake3.New(32, key).XOF()
181 if _, err = io.ReadFull(rnd, our); err != nil {
184 if _, err = connTLS.Write(our); err != nil {
187 if _, err = io.ReadFull(connTLS, their); err != nil {
190 if !bytes.Equal(our, their) {
191 logger.Error("pong mismatch")
200 logger.Info("finishing",
205 delete(Peers, localAddr.String())
206 txs <- udpobfs.Buf{Buf: nil}
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)[:])
280 txs = Peers[from.String()]
282 txs <- udpobfs.Buf{Buf: buf, N: n}
285 neu := make([]byte, n)
286 copy(neu, (*buf)[:n])
288 go newPeer(from, neu)