Skip to content

Commit 1fb8c22

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

File tree

12 files changed

+571
-0
lines changed

12 files changed

+571
-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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
19+
source-repository head
20+
Type: git
21+
Location: https://github.com/haskell-distributed/distributed-process
22+
SubDir: packages/network-transport-quic
23+
24+
common common
25+
ghc-options:
26+
-- warnings
27+
-Wall
28+
-Wcompat
29+
-Widentities
30+
-Wincomplete-uni-patterns
31+
-Wincomplete-record-updates
32+
-Wredundant-constraints
33+
-fhide-source-paths
34+
-Wpartial-fields
35+
-Wunused-packages
36+
-- The -threaded option is /required/ to use the quic library
37+
-threaded
38+
39+
library
40+
import: common
41+
build-depends: attoparsec
42+
, base >= 4.14 && < 5
43+
, bytestring >= 0.10 && < 0.13
44+
, containers
45+
, ip
46+
, network >= 3.1 && < 3.3
47+
, network-transport >= 0.5 && < 0.6
48+
, quic ^>=0.2
49+
, stm >=2.4 && <2.6
50+
, text >= 2.0 && <2.2
51+
, tls
52+
, tls-session-manager
53+
exposed-modules: Network.Transport.QUIC
54+
Network.Transport.QUIC.Internal
55+
other-modules: Network.Transport.QUIC.Internal.QUICAddr
56+
Network.Transport.QUIC.Internal.TLS
57+
Network.Transport.QUIC.Internal.TransportState
58+
default-language: Haskell2010
59+
default-extensions: ImportQualifiedPost
60+
-- The -threaded option is /required/ to use the quic library
61+
hs-source-dirs: src
62+
63+
test-suite network-transport-quic-tests
64+
import: common
65+
default-language: Haskell2010
66+
default-extensions: ImportQualifiedPost
67+
main-is: Main.hs
68+
other-modules: Test.Network.Transport.QUIC
69+
Test.Network.Transport.QUIC.Internal.QUICAddr
70+
type: exitcode-stdio-1.0
71+
hs-source-dirs: test
72+
build-depends: base
73+
, hedgehog
74+
, ip
75+
, network
76+
, network-transport
77+
, network-transport-quic
78+
, network-transport-tests
79+
, tasty ^>=1.5
80+
, tasty-hedgehog
81+
, tasty-hunit
82+
, text
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Network.Transport.QUIC (
2+
createTransport,
3+
QUICAddr (..),
4+
) where
5+
6+
import Network.Transport.QUIC.Internal (
7+
QUICAddr (..),
8+
createTransport,
9+
)
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
{-# LANGUAGE LambdaCase #-}
2+
{-# LANGUAGE ScopedTypeVariables #-}
3+
4+
module Network.Transport.QUIC.Internal (
5+
createTransport,
6+
QUICAddr (..),
7+
encodeQUICAddr,
8+
decodeQUICAddr,
9+
) where
10+
11+
import Control.Concurrent (ThreadId, forkIO, killThread, myThreadId)
12+
import Control.Concurrent.STM (atomically)
13+
import Control.Concurrent.STM.TQueue (
14+
TQueue,
15+
newTQueueIO,
16+
readTQueue,
17+
writeTQueue,
18+
)
19+
import Control.Exception (bracket, try)
20+
import Control.Monad (void)
21+
import Data.Bifunctor (first)
22+
import Data.ByteString (StrictByteString)
23+
import Data.ByteString qualified as BS
24+
import Data.Foldable (traverse_)
25+
import Data.Functor (($>), (<&>))
26+
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
27+
import Data.Set (Set)
28+
import Data.Set qualified as Set
29+
import GHC.IORef (atomicModifyIORef'_)
30+
import Network.QUIC (Stream)
31+
import Network.QUIC qualified as QUIC
32+
import Network.QUIC.Client (defaultClientConfig)
33+
import Network.QUIC.Client qualified as QUIC.Client
34+
import Network.QUIC.Server (defaultServerConfig)
35+
import Network.QUIC.Server qualified as QUIC.Server
36+
import Network.TLS (Credentials (Credentials))
37+
import Network.Transport (ConnectErrorCode (ConnectNotFound), ConnectHints, Connection (..), ConnectionId, EndPoint (..), EndPointAddress, Event (..), NewEndPointErrorCode (NewEndPointFailed), NewMulticastGroupErrorCode (NewMulticastGroupUnsupported), Reliability, ResolveMulticastGroupErrorCode (ResolveMulticastGroupUnsupported), SendErrorCode (..), Transport (..), TransportError (..))
38+
import Network.Transport.QUIC.Internal.QUICAddr (QUICAddr (..), decodeQUICAddr, encodeQUICAddr)
39+
import Network.Transport.QUIC.Internal.TLS qualified as TLS
40+
import Network.Transport.QUIC.Internal.TransportState (TransportState, newTransportState, registerEndpoint, traverseTransportState)
41+
42+
-- | Create a new Transport.
43+
--
44+
-- Only a single transport should be created per Haskell process
45+
-- (threads can, and should, create their own endpoints though).
46+
createTransport ::
47+
QUICAddr ->
48+
-- | Path to certificate
49+
FilePath ->
50+
-- | Path to key
51+
FilePath ->
52+
IO Transport
53+
createTransport quicAddr certFile keyFile = do
54+
transportState <- newTransportState
55+
pure $
56+
Transport
57+
(newEndpoint transportState quicAddr certFile keyFile)
58+
(closeQUICTransport transportState)
59+
60+
newEndpoint ::
61+
TransportState ->
62+
QUICAddr ->
63+
-- | Path to certificate
64+
FilePath ->
65+
-- | Path to key
66+
FilePath ->
67+
IO (Either (TransportError NewEndPointErrorCode) EndPoint)
68+
newEndpoint transportState quicAddr@(QUICAddr host port) certFile keyFile = do
69+
eventQueue <- newTQueueIO
70+
71+
state <- EndpointState <$> newIORef mempty
72+
tlsSessionManager <- TLS.sessionManager
73+
TLS.credentialLoadX509 certFile keyFile >>= \case
74+
Left errmsg -> pure . Left $ TransportError NewEndPointFailed errmsg
75+
Right creds -> do
76+
serverThread <-
77+
forkIO $
78+
QUIC.Server.run
79+
( defaultServerConfig
80+
{ QUIC.Server.scAddresses = [(read host, read port)]
81+
, QUIC.Server.scSessionManager = tlsSessionManager
82+
, QUIC.Server.scCredentials = Credentials [creds]
83+
}
84+
)
85+
( withQUICStream $
86+
-- TODO: create a bidirectional stream
87+
-- which can be re-used for sending
88+
\stream ->
89+
-- We register which threads are actively receiving or sending
90+
-- data such that we can cleanly stop
91+
withThreadRegistered state $ do
92+
-- TODO: how to ensure positivity of ConnectionId? QUIC StreamID should be a 62 bit integer,
93+
-- so there's room to make it a positive 64 bit integer (ConnectionId ~ Word64)
94+
let connId = fromIntegral (QUIC.streamId stream)
95+
receiveLoop connId stream eventQueue
96+
)
97+
98+
let endpoint =
99+
EndPoint
100+
(atomically (readTQueue eventQueue))
101+
(encodeQUICAddr quicAddr)
102+
connectQUIC
103+
(pure . Left $ TransportError NewMulticastGroupUnsupported "Multicast not supported")
104+
(pure . Left . const (TransportError ResolveMulticastGroupUnsupported "Multicast not supported"))
105+
(stopAllThreads state >> killThread serverThread)
106+
void $ transportState `registerEndpoint` endpoint
107+
pure $ Right endpoint
108+
where
109+
receiveLoop ::
110+
ConnectionId ->
111+
QUIC.Stream ->
112+
TQueue Event ->
113+
IO ()
114+
receiveLoop connId stream eventQueue = do
115+
incoming <- QUIC.recvStream stream 1024 -- TODO: variable length?
116+
-- TODO: check some state whether we should stop all connections
117+
if BS.null incoming
118+
then do
119+
atomically (writeTQueue eventQueue (ConnectionClosed connId))
120+
else do
121+
atomically (writeTQueue eventQueue (Received connId [incoming]))
122+
receiveLoop connId stream eventQueue
123+
124+
withQUICStream :: (QUIC.Stream -> IO a) -> QUIC.Connection -> IO a
125+
withQUICStream f conn =
126+
bracket
127+
(QUIC.waitEstablished conn >> QUIC.acceptStream conn)
128+
(\stream -> QUIC.closeStream stream >> QUIC.Server.stop conn)
129+
f
130+
131+
connectQUIC ::
132+
EndPointAddress ->
133+
Reliability ->
134+
ConnectHints ->
135+
IO (Either (TransportError ConnectErrorCode) Connection)
136+
connectQUIC endpointAddress _reliability _connectHints =
137+
case decodeQUICAddr endpointAddress of
138+
Left errmsg -> pure $ Left $ TransportError ConnectNotFound ("Could not decode QUIC address: " <> errmsg)
139+
Right (QUICAddr hostname port) ->
140+
try $ do
141+
let clientConfig =
142+
defaultClientConfig
143+
{ QUIC.Client.ccServerName = hostname
144+
, QUIC.Client.ccPortName = port
145+
}
146+
147+
-- TODO: why is the TLS handshake failing?
148+
QUIC.Client.run clientConfig $ \conn -> do
149+
QUIC.waitEstablished conn
150+
stream <- QUIC.stream conn
151+
152+
pure $
153+
Connection
154+
(sendQUIC stream)
155+
(QUIC.closeStream stream)
156+
where
157+
sendQUIC :: Stream -> [StrictByteString] -> IO (Either (TransportError SendErrorCode) ())
158+
sendQUIC stream payloads =
159+
try (QUIC.sendStreamMany stream payloads)
160+
<&> first
161+
( \case
162+
QUIC.StreamIsClosed -> TransportError SendClosed "QUIC stream is closed"
163+
QUIC.ConnectionIsClosed reason -> TransportError SendClosed (show reason)
164+
other -> TransportError SendFailed (show other)
165+
)
166+
167+
closeQUICTransport :: TransportState -> IO ()
168+
closeQUICTransport = flip traverseTransportState (\_ endpoint -> closeEndPoint endpoint)
169+
170+
{- | We keep track of all threads actively listening on QUIC streams
171+
so that we can cleanly stop these threads when closing the endpoint.
172+
173+
See 'withThreadRegistered' for a combinator which automatically keeps
174+
track of these threads
175+
-}
176+
newtype EndpointState = EndpointState
177+
{ threads :: IORef (Set ThreadId)
178+
}
179+
180+
withThreadRegistered :: EndpointState -> IO a -> IO a
181+
withThreadRegistered state f =
182+
bracket
183+
registerThread
184+
unregisterThread
185+
(const f)
186+
where
187+
registerThread =
188+
myThreadId
189+
>>= \tid ->
190+
atomicModifyIORef'_ (threads state) (Set.insert tid)
191+
$> tid
192+
193+
unregisterThread tid =
194+
atomicModifyIORef'_ (threads state) (Set.insert tid)
195+
196+
stopAllThreads :: EndpointState -> IO ()
197+
stopAllThreads (EndpointState tds) = do
198+
readIORef tds >>= traverse_ killThread
199+
writeIORef tds mempty -- so that we can call `closeQUICTransport` even after the endpoint has been closed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module Network.Transport.QUIC.Internal.QUICAddr (
2+
QUICAddr (..),
3+
encodeQUICAddr,
4+
decodeQUICAddr,
5+
) where
6+
7+
import Data.Attoparsec.Text (Parser, endOfInput, parseOnly, (<?>))
8+
import Data.Attoparsec.Text qualified as A
9+
import Data.ByteString.Char8 qualified as BS8
10+
import Data.Text qualified as Text (unpack)
11+
import Data.Text.Encoding (decodeUtf8Lenient)
12+
import Net.IPv4 (IPv4)
13+
import Net.IPv4 qualified as IPv4
14+
import Net.IPv6 (IPv6)
15+
import Net.IPv6 qualified as IPv6
16+
import Network.Socket (HostName, ServiceName)
17+
import Network.Transport (EndPointAddress (EndPointAddress))
18+
19+
data QUICAddr = QUICAddr
20+
{ quicBindHost :: !HostName
21+
, quicBindPort :: !ServiceName
22+
}
23+
deriving (Eq, Ord, Show)
24+
25+
-- | Encode a 'QUICAddr' to 'EndPointAddress'
26+
encodeQUICAddr :: QUICAddr -> EndPointAddress
27+
encodeQUICAddr (QUICAddr host port) =
28+
EndPointAddress
29+
(BS8.pack $ host <> ":" <> port)
30+
31+
-- | Decode a 'QUICAddr' from an 'EndPointAddress'
32+
decodeQUICAddr :: EndPointAddress -> Either String QUICAddr
33+
decodeQUICAddr (EndPointAddress bytes) =
34+
parseOnly (parser <* endOfInput) (decodeUtf8Lenient bytes)
35+
where
36+
parser =
37+
QUICAddr
38+
<$> (parseHostName <* A.char ':')
39+
<*> parseServiceName
40+
41+
parseHostName :: Parser HostName
42+
parseHostName =
43+
renderHostNameChoice
44+
<$> A.choice
45+
[ IPV6 <$> IPv6.parser <?> "IPv6"
46+
, IPV4 <$> IPv4.parser <?> "IPv6"
47+
, (Named . Text.unpack <$> A.takeTill (== ':')) <?> "Named host"
48+
]
49+
<?> "Host name"
50+
51+
parseServiceName :: Parser ServiceName
52+
parseServiceName = Text.unpack <$> A.takeText <?> "Service name"
53+
54+
data HostNameChoice
55+
= IPV4 IPv4
56+
| IPV6 IPv6
57+
| Named HostName
58+
59+
renderHostNameChoice :: HostNameChoice -> HostName
60+
renderHostNameChoice (IPV4 ipv4) = IPv4.encodeString ipv4
61+
renderHostNameChoice (IPV6 ipv6) = Text.unpack $ IPv6.encode ipv6
62+
renderHostNameChoice (Named hostName) = hostName
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Network.Transport.QUIC.Internal.TLS (
2+
-- * TLS session manager
3+
sessionManager,
4+
5+
-- * Loading TLS credentials
6+
credentialLoadX509,
7+
) where
8+
9+
import Network.TLS (SessionManager, credentialLoadX509)
10+
import Network.TLS.SessionManager (defaultConfig, newSessionManager)
11+
12+
sessionManager :: IO SessionManager
13+
sessionManager = newSessionManager defaultConfig

0 commit comments

Comments
 (0)