Skip to content

Commit b0f9529

Browse files
committed
Work-in-progress
1 parent 2edcdf0 commit b0f9529

File tree

14 files changed

+728
-0
lines changed

14 files changed

+728
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) Laurent P. René de Cotret
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included
12+
in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
cabal-version: 3.0
2+
Name: network-transport-quic
3+
Version: 0.1.0
4+
build-Type: Simple
5+
License: BSD-3-Clause
6+
License-file: LICENSE
7+
Copyright: Laurent P. René de Cotret
8+
Author: Laurent P. René de Cotret
9+
maintainer: The Distributed Haskell team
10+
Stability: experimental
11+
Homepage: http://haskell-distributed.github.com
12+
Bug-Reports: https://github.com/haskell-distributed/distributed-process/issues
13+
Synopsis: Networking layer for Cloud Haskell based on QUIC
14+
Description: Networking layer for Cloud Haskell based on QUIC
15+
tested-with: GHC==8.10.7 GHC==9.0.2 GHC==9.2.8 GHC==9.4.5 GHC==9.6.4 GHC==9.8.2 GHC==9.10.1 GHC==9.12.1
16+
Category: Network
17+
extra-doc-files: ChangeLog
18+
extra-source-files: test/credentials/*
19+
20+
source-repository head
21+
Type: git
22+
Location: https://github.com/haskell-distributed/distributed-process
23+
SubDir: packages/network-transport-quic
24+
25+
common common
26+
ghc-options:
27+
-- warnings
28+
-Wall
29+
-Wcompat
30+
-Widentities
31+
-Wincomplete-uni-patterns
32+
-Wincomplete-record-updates
33+
-Wredundant-constraints
34+
-fhide-source-paths
35+
-Wpartial-fields
36+
-Wunused-packages
37+
-- The -threaded option is /required/ to use the quic library
38+
-threaded
39+
40+
library
41+
import: common
42+
build-depends: attoparsec
43+
, base >= 4.14 && < 5
44+
, bytestring >= 0.10 && < 0.13
45+
, containers
46+
, ip
47+
, network >= 3.1 && < 3.3
48+
, network-transport >= 0.5 && < 0.6
49+
, quic ^>=0.2
50+
, stm >=2.4 && <2.6
51+
, text >= 2.0 && <2.2
52+
, tls
53+
, tls-session-manager
54+
exposed-modules: Network.Transport.QUIC
55+
Network.Transport.QUIC.Internal
56+
other-modules: Network.Transport.QUIC.Internal.Configuration
57+
Network.Transport.QUIC.Internal.EndpointState
58+
Network.Transport.QUIC.Internal.QUICAddr
59+
Network.Transport.QUIC.Internal.TLS
60+
Network.Transport.QUIC.Internal.TransportState
61+
default-language: Haskell2010
62+
default-extensions: ImportQualifiedPost
63+
-- The -threaded option is /required/ to use the quic library
64+
hs-source-dirs: src
65+
66+
test-suite network-transport-quic-tests
67+
import: common
68+
default-language: Haskell2010
69+
default-extensions: ImportQualifiedPost
70+
main-is: Main.hs
71+
other-modules: Test.Network.Transport.QUIC
72+
Test.Network.Transport.QUIC.Internal.QUICAddr
73+
type: exitcode-stdio-1.0
74+
hs-source-dirs: test
75+
build-depends: base
76+
, filepath
77+
, hedgehog
78+
, ip
79+
, network
80+
, network-transport
81+
, network-transport-quic
82+
, network-transport-tests
83+
, tasty ^>=1.5
84+
, tasty-hedgehog
85+
, tasty-hunit
86+
, text
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module Network.Transport.QUIC (
2+
createTransport,
3+
QUICAddr (..),
4+
5+
-- * Re-export to generate credentials
6+
Credential,
7+
credentialLoadX509,
8+
) where
9+
10+
import Network.Transport.QUIC.Internal (
11+
-- \* Re-export to generate credentials
12+
Credential,
13+
QUICAddr (..),
14+
createTransport,
15+
credentialLoadX509,
16+
)
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
{-# LANGUAGE LambdaCase #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE ScopedTypeVariables #-}
4+
5+
module Network.Transport.QUIC.Internal (
6+
createTransport,
7+
QUICAddr (..),
8+
encodeQUICAddr,
9+
decodeQUICAddr,
10+
11+
-- * Re-export to generate credentials
12+
Credential,
13+
credentialLoadX509,
14+
) where
15+
16+
import Control.Concurrent (ThreadId, forkFinally, killThread, myThreadId)
17+
import Control.Concurrent.STM (atomically)
18+
import Control.Concurrent.STM.TQueue (
19+
TQueue,
20+
newTQueueIO,
21+
readTQueue,
22+
writeTQueue,
23+
)
24+
import Control.Exception (bracket, try)
25+
import Control.Monad (void)
26+
import Data.Bifunctor (first)
27+
import Data.ByteString (StrictByteString)
28+
import Data.ByteString qualified as BS
29+
import Data.Foldable (traverse_)
30+
import Data.Functor (($>), (<&>))
31+
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
32+
import Data.List.NonEmpty (NonEmpty)
33+
import Data.Set (Set)
34+
import Data.Set qualified as Set
35+
import GHC.IORef (atomicModifyIORef'_)
36+
import Network.QUIC (Stream)
37+
import Network.QUIC qualified as QUIC
38+
import Network.QUIC.Client qualified as QUIC.Client
39+
import Network.QUIC.Server qualified as QUIC.Server
40+
import Network.TLS (Credential)
41+
import Network.Transport (
42+
ConnectErrorCode (ConnectNotFound),
43+
ConnectHints,
44+
Connection (..),
45+
ConnectionId,
46+
EndPoint (..),
47+
EndPointAddress,
48+
Event (..),
49+
EventErrorCode (EventEndPointFailed),
50+
NewEndPointErrorCode,
51+
NewMulticastGroupErrorCode (NewMulticastGroupUnsupported),
52+
Reliability,
53+
ResolveMulticastGroupErrorCode (ResolveMulticastGroupUnsupported),
54+
SendErrorCode (..),
55+
Transport (..),
56+
TransportError (..),
57+
)
58+
import Network.Transport.QUIC.Internal.Configuration (credentialLoadX509, mkClientConfig, mkServerConfig)
59+
import Network.Transport.QUIC.Internal.QUICAddr (QUICAddr (..), decodeQUICAddr, encodeQUICAddr)
60+
import Network.Transport.QUIC.Internal.TransportState (TransportState, newTransportState, registerEndpoint, traverseTransportState)
61+
62+
{- | Create a new Transport.
63+
64+
Only a single transport should be created per Haskell process
65+
(threads can, and should, create their own endpoints though).
66+
-}
67+
createTransport ::
68+
QUICAddr ->
69+
NonEmpty Credential ->
70+
IO Transport
71+
createTransport quicAddr creds = do
72+
transportState <- newTransportState
73+
pure $
74+
Transport
75+
(newEndpoint transportState quicAddr creds)
76+
(closeQUICTransport transportState)
77+
78+
newEndpoint ::
79+
TransportState ->
80+
QUICAddr ->
81+
NonEmpty Credential ->
82+
IO (Either (TransportError NewEndPointErrorCode) EndPoint)
83+
newEndpoint transportState quicAddr@(QUICAddr host port) creds = do
84+
eventQueue <- newTQueueIO
85+
86+
state <- EndpointState <$> newIORef mempty
87+
88+
serverConfig <- mkServerConfig host port creds
89+
serverThread <-
90+
forkFinally
91+
( QUIC.Server.run
92+
serverConfig
93+
( withQUICStream $
94+
-- TODO: create a bidirectional stream
95+
-- which can be re-used for sending
96+
\stream ->
97+
-- We register which threads are actively receiving or sending
98+
-- data such that we can cleanly stop
99+
withThreadRegistered state $ do
100+
-- TODO: how to ensure positivity of ConnectionId? QUIC StreamID should be a 62 bit integer,
101+
-- so there's room to make it a positive 64 bit integer (ConnectionId ~ Word64)
102+
let connId = fromIntegral (QUIC.streamId stream)
103+
receiveLoop connId stream eventQueue
104+
)
105+
)
106+
( \case
107+
Left exc -> atomically (writeTQueue eventQueue (ErrorEvent (TransportError EventEndPointFailed (show exc))))
108+
Right _ -> pure ()
109+
)
110+
111+
let endpoint =
112+
EndPoint
113+
(atomically (readTQueue eventQueue))
114+
(encodeQUICAddr quicAddr)
115+
(connectQUIC creds)
116+
(pure . Left $ TransportError NewMulticastGroupUnsupported "Multicast not supported")
117+
(pure . Left . const (TransportError ResolveMulticastGroupUnsupported "Multicast not supported"))
118+
(stopAllThreads state >> killThread serverThread >> atomically (writeTQueue eventQueue EndPointClosed))
119+
void $ transportState `registerEndpoint` endpoint
120+
pure $ Right endpoint
121+
where
122+
receiveLoop ::
123+
ConnectionId ->
124+
QUIC.Stream ->
125+
TQueue Event ->
126+
IO ()
127+
receiveLoop connId stream eventQueue = do
128+
incoming <- QUIC.recvStream stream 1024 -- TODO: variable length?
129+
-- TODO: check some state whether we should stop all connections
130+
if BS.null incoming
131+
then do
132+
atomically (writeTQueue eventQueue (ConnectionClosed connId))
133+
else do
134+
atomically (writeTQueue eventQueue (Received connId [incoming]))
135+
receiveLoop connId stream eventQueue
136+
137+
withQUICStream :: (QUIC.Stream -> IO a) -> QUIC.Connection -> IO a
138+
withQUICStream f conn =
139+
bracket
140+
(QUIC.waitEstablished conn >> QUIC.acceptStream conn)
141+
(\stream -> QUIC.closeStream stream >> QUIC.Server.stop conn)
142+
f
143+
144+
connectQUIC ::
145+
NonEmpty Credential ->
146+
EndPointAddress ->
147+
Reliability ->
148+
ConnectHints ->
149+
IO (Either (TransportError ConnectErrorCode) Connection)
150+
connectQUIC creds endpointAddress _reliability _connectHints =
151+
case decodeQUICAddr endpointAddress of
152+
Left errmsg -> pure $ Left $ TransportError ConnectNotFound ("Could not decode QUIC address: " <> errmsg)
153+
Right (QUICAddr hostname port) ->
154+
try $ do
155+
clientConfig <- mkClientConfig hostname port creds
156+
157+
QUIC.Client.run clientConfig $ \conn -> do
158+
QUIC.waitEstablished conn
159+
stream <- QUIC.stream conn
160+
161+
pure $
162+
Connection
163+
(sendQUIC stream)
164+
(QUIC.closeStream stream)
165+
where
166+
sendQUIC :: Stream -> [StrictByteString] -> IO (Either (TransportError SendErrorCode) ())
167+
sendQUIC stream payloads =
168+
try (QUIC.sendStreamMany stream payloads)
169+
<&> first
170+
( \case
171+
QUIC.StreamIsClosed -> TransportError SendClosed "QUIC stream is closed"
172+
QUIC.ConnectionIsClosed reason -> TransportError SendClosed (show reason)
173+
other -> TransportError SendFailed (show other)
174+
)
175+
176+
closeQUICTransport :: TransportState -> IO ()
177+
closeQUICTransport = flip traverseTransportState (\_ endpoint -> closeEndPoint endpoint)
178+
179+
{- | We keep track of all threads actively listening on QUIC streams
180+
so that we can cleanly stop these threads when closing the endpoint.
181+
182+
See 'withThreadRegistered' for a combinator which automatically keeps
183+
track of these threads
184+
-}
185+
newtype EndpointState = EndpointState
186+
{ threads :: IORef (Set ThreadId)
187+
}
188+
189+
withThreadRegistered :: EndpointState -> IO a -> IO a
190+
withThreadRegistered state f =
191+
bracket
192+
registerThread
193+
unregisterThread
194+
(const f)
195+
where
196+
registerThread =
197+
myThreadId
198+
>>= \tid ->
199+
atomicModifyIORef'_ (threads state) (Set.insert tid)
200+
$> tid
201+
202+
unregisterThread tid =
203+
atomicModifyIORef'_ (threads state) (Set.insert tid)
204+
205+
stopAllThreads :: EndpointState -> IO ()
206+
stopAllThreads (EndpointState tds) = do
207+
readIORef tds >>= traverse_ killThread
208+
writeIORef tds mempty -- so that we can call `closeQUICTransport` even after the endpoint has been closed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Network.Transport.QUIC.Internal.Configuration (
4+
mkClientConfig,
5+
mkServerConfig,
6+
7+
-- * Re-export to generate credentials
8+
Credential,
9+
TLS.credentialLoadX509,
10+
) where
11+
12+
import Data.List.NonEmpty (NonEmpty)
13+
import Data.List.NonEmpty qualified as NonEmpty
14+
import Network.QUIC.Client (ClientConfig (ccALPN, ccValidate, ccWatchDog), ccPortName, ccServerName, defaultClientConfig)
15+
import Network.QUIC.Internal (ClientConfig (ccDebugLog), Milliseconds (Milliseconds), ServerConfig (scALPN), ccCredentials, ccKeyLog, maxIdleTimeout, scParameters)
16+
import Network.QUIC.Server (ServerConfig (scAddresses, scCredentials, scSessionManager, scUse0RTT), defaultServerConfig)
17+
import Network.Socket (HostName, ServiceName)
18+
import Network.TLS (Credential, Credentials (Credentials))
19+
import Network.Transport.QUIC.Internal.TLS qualified as TLS
20+
21+
mkClientConfig ::
22+
HostName ->
23+
ServiceName ->
24+
NonEmpty Credential ->
25+
IO ClientConfig
26+
mkClientConfig host port creds = do
27+
pure $
28+
defaultClientConfig
29+
{ ccServerName = host
30+
, ccPortName = port
31+
, ccALPN = \_version -> pure (Just ["perf"])
32+
, ccValidate = False
33+
, ccCredentials = Credentials (NonEmpty.toList creds)
34+
, ccWatchDog = True
35+
, -- The following two parameters are for debugging. TODO: turn off by default
36+
ccDebugLog = True
37+
, ccKeyLog = putStrLn
38+
}
39+
40+
mkServerConfig ::
41+
HostName ->
42+
ServiceName ->
43+
NonEmpty Credential ->
44+
IO ServerConfig
45+
mkServerConfig host port creds = do
46+
tlsSessionManager <- TLS.sessionManager
47+
48+
pure $
49+
defaultServerConfig
50+
{ scAddresses = [(read host, read port)]
51+
, scSessionManager = tlsSessionManager
52+
, scCredentials = Credentials (NonEmpty.toList creds)
53+
, scALPN = Just $ \_version _protocols -> pure "perf"
54+
, scUse0RTT = True
55+
, scParameters =
56+
(scParameters defaultServerConfig)
57+
{ maxIdleTimeout = Milliseconds 1000
58+
}
59+
}

0 commit comments

Comments
 (0)