diff options
Diffstat (limited to 'dht/src/Network/Tox/Onion/Routes.hs')
-rw-r--r-- | dht/src/Network/Tox/Onion/Routes.hs | 730 |
1 files changed, 730 insertions, 0 deletions
diff --git a/dht/src/Network/Tox/Onion/Routes.hs b/dht/src/Network/Tox/Onion/Routes.hs new file mode 100644 index 00000000..c054b99e --- /dev/null +++ b/dht/src/Network/Tox/Onion/Routes.hs | |||
@@ -0,0 +1,730 @@ | |||
1 | {-# LANGUAGE LambdaCase #-} | ||
2 | {-# LANGUAGE NondecreasingIndentation #-} | ||
3 | {-# LANGUAGE RankNTypes #-} | ||
4 | module Network.Tox.Onion.Routes where | ||
5 | |||
6 | import Control.Concurrent.ThreadUtil | ||
7 | import Crypto.Tox | ||
8 | import Network.Address | ||
9 | import Network.Kademlia | ||
10 | import Network.Kademlia.Bootstrap | ||
11 | import Network.Kademlia.Routing as R | ||
12 | import Network.Kademlia.Search | ||
13 | import Network.QueryResponse | ||
14 | import Network.QueryResponse.TCP | ||
15 | import Network.Tox.NodeId | ||
16 | import Network.Tox.Onion.Transport as Onion | ||
17 | import qualified Data.Tox.Relay as TCP | ||
18 | import qualified Network.Tox.TCP as TCP | ||
19 | import qualified TCPProber as TCP | ||
20 | |||
21 | import Control.Arrow | ||
22 | import Control.Concurrent.STM | ||
23 | import Control.Concurrent.STM.TArray | ||
24 | import Control.Monad | ||
25 | import Crypto.Random | ||
26 | import Data.Array.MArray | ||
27 | import Data.Bits | ||
28 | import Data.Bool | ||
29 | import Data.List | ||
30 | import qualified Data.ByteString as B | ||
31 | import Data.Functor.Identity | ||
32 | import Data.Hashable | ||
33 | import qualified Data.HashMap.Strict as HashMap | ||
34 | ;import Data.HashMap.Strict (HashMap) | ||
35 | import qualified Data.IntMap as IntMap | ||
36 | ;import Data.IntMap (IntMap) | ||
37 | import Data.Maybe | ||
38 | import Data.Ord | ||
39 | import qualified Data.Serialize as S | ||
40 | import Data.Time.Clock.POSIX | ||
41 | import Data.Typeable | ||
42 | import Data.Word | ||
43 | import qualified Data.Word64Map as W64 | ||
44 | ;import Data.Word64Map (Word64Map, fitsInInt) | ||
45 | import Network.Socket | ||
46 | import System.Endian | ||
47 | import System.Timeout | ||
48 | |||
49 | -- Toxcore saves a maximum of 12 paths: 6 paths are reserved for announcing | ||
50 | -- ourselves and 6 others are used to search for friends. | ||
51 | -- | ||
52 | -- Note: This is pointless because a man-in-the-middle attack currently makes | ||
53 | -- it trivial to glean friend relationships: the storing node can swap the | ||
54 | -- published to-route key with his own giving him access to one layer of | ||
55 | -- encryption and thus the real public key of the sender. TODO: | ||
56 | -- Counter-measures. | ||
57 | -- | ||
58 | -- Unlike toxcore, we don't currently reserve paths for only-searching or | ||
59 | -- only-announcing. Instead, we maintain 12 multi-purpose routes. | ||
60 | data OnionRouter = OnionRouter | ||
61 | { -- | For every query, we remember the destination and source keys | ||
62 | -- so we can decrypt the response. | ||
63 | pendingQueries :: TVar (Word64Map PendingQuery) | ||
64 | -- | The current 12 routes that may be assigned to outgoing packets. | ||
65 | , routeMap :: TArray Int (Maybe RouteRecord) | ||
66 | -- | A set of nodes used to query for random route nodes. These aren't | ||
67 | -- used directly in onion routes, they are queried for route nodes that | ||
68 | -- are nearby randomly selected ids. | ||
69 | , trampolinesUDP :: TrampolineSet NodeInfo | ||
70 | -- | A set for TCP relays to use as trampolines when UDP is not available. | ||
71 | , trampolinesTCP :: TrampolineSet TCP.NodeInfo | ||
72 | -- | True when we need to rely on TCP relays because UDP is apparently unavailable. | ||
73 | , tcpMode :: TVar (Maybe Bool) -- Nothing: tcp disabled, False: use trampolinesUDP, True: use trampolinesTCP | ||
74 | -- | The pseudo-random generator used to select onion routes. | ||
75 | , onionDRG :: TVar ChaChaDRG | ||
76 | -- | Building onion routes happens in a dedicated thread. See 'forkRouteBuilder'. | ||
77 | , routeThread :: ThreadId | ||
78 | -- | Each of the 12 routes has a version number here that is set larger | ||
79 | -- than the 'routeVersion' set in 'routeMap' when the route should be | ||
80 | -- discarded and replaced with a fresh one. | ||
81 | , pendingRoutes :: TArray Int Int | ||
82 | -- | Parameters used to implement Kademlia for TCP relays. | ||
83 | , tcpKademliaClient :: TCP.TCPClient String Nonce8 | ||
84 | -- | This thread maintains the TCP relay table. | ||
85 | , tcpKademliaThread :: ThreadId | ||
86 | , tcpProberState :: TCPCache (SessionProtocol TCP.RelayPacket TCP.RelayPacket) | ||
87 | , tcpProber :: TCP.TCPProber | ||
88 | , tcpProberThread :: ThreadId | ||
89 | -- | Kademlia table of TCP relays. | ||
90 | , tcpBucketRefresher :: BucketRefresher NodeId TCP.NodeInfo | ||
91 | -- | Debug prints are written to this channel which is then flushed to | ||
92 | -- 'routeLogger'. | ||
93 | , routeLog :: TChan String | ||
94 | -- | User supplied log function. | ||
95 | , routeLogger :: String -> IO () | ||
96 | } | ||
97 | |||
98 | data PendingQuery = PendingQuery | ||
99 | { pendingVersion :: !Int -- ^ Remembered version number so timeouts can signal a rebuild. | ||
100 | , pendingDestination :: OnionDestination RouteId | ||
101 | } | ||
102 | deriving Show | ||
103 | |||
104 | data RouteRecord = RouteRecord | ||
105 | { storedRoute :: OnionRoute | ||
106 | , responseCount :: !Int | ||
107 | , timeoutCount :: !Int | ||
108 | , routeVersion :: !Int | ||
109 | , routeBirthTime :: !POSIXTime | ||
110 | } | ||
111 | deriving Show | ||
112 | |||
113 | -- Onion paths have different timeouts depending on whether the path is | ||
114 | -- confirmed or unconfirmed. Unconfirmed paths (paths that core has never | ||
115 | -- received any responses from) have a timeout of 4 seconds with 2 tries before | ||
116 | -- they are deemed non working. This is because, due to network conditions, | ||
117 | -- there may be a large number of newly created paths that do not work and so | ||
118 | -- trying them a lot would make finding a working path take much longer. The | ||
119 | -- timeout for a confirmed path (from which a response was received) is 12 | ||
120 | -- seconds with 4 tries without a response. A confirmed path has a maximum | ||
121 | -- lifetime of 1200 seconds to make possible deanonimization attacks more | ||
122 | -- difficult. | ||
123 | timeoutForRoute :: RouteRecord -> Int | ||
124 | timeoutForRoute RouteRecord{ responseCount = 0 } = 4000000 | ||
125 | timeoutForRoute RouteRecord{ responseCount = _ } = 12000000 | ||
126 | |||
127 | freshRoute :: POSIXTime -> OnionRoute -> Maybe RouteRecord -> Maybe RouteRecord | ||
128 | freshRoute birthday r mrec = Just $ RouteRecord | ||
129 | { storedRoute = r | ||
130 | , responseCount = 0 | ||
131 | , timeoutCount = 0 | ||
132 | , routeVersion = maybe 0 succ $ routeVersion <$> mrec | ||
133 | , routeBirthTime = birthday | ||
134 | } | ||
135 | |||
136 | modifyArray :: TArray Int r -> (r -> r) -> Int -> STM () | ||
137 | modifyArray a f i = do | ||
138 | mx <- readArray a i | ||
139 | writeArray a i $ f mx | ||
140 | {-# INLINE modifyArray #-} | ||
141 | |||
142 | gotResponse :: RouteRecord -> RouteRecord | ||
143 | gotResponse rr = rr | ||
144 | { responseCount = succ $ responseCount rr | ||
145 | , timeoutCount = 0 | ||
146 | } | ||
147 | |||
148 | gotTimeout :: RouteRecord -> RouteRecord | ||
149 | gotTimeout rr = rr | ||
150 | { timeoutCount = succ $ timeoutCount rr | ||
151 | } | ||
152 | |||
153 | newtype RouteEvent = BuildRoute RouteId | ||
154 | |||
155 | newOnionRouter :: TransportCrypto | ||
156 | -> (String -> IO ()) | ||
157 | -> Bool -- is tcp enabled? | ||
158 | -> IO ( OnionRouter | ||
159 | , TVar ( ChaChaDRG | ||
160 | , Word64Map (Either (Maybe (Bool,TCP.RelayPacket) -> IO ()) | ||
161 | (Maybe (OnionMessage Identity) -> IO ())))) | ||
162 | newOnionRouter crypto perror tcp_enabled = do | ||
163 | drg0 <- drgNew | ||
164 | (rlog,pq,rm) <- atomically $ do | ||
165 | rlog <- newTChan | ||
166 | pq <- newTVar W64.empty | ||
167 | rm <- newArray (0,11) Nothing | ||
168 | return (rlog,pq,rm) | ||
169 | ((tbl,(tcptbl,tcpcons)),tcp) <- do | ||
170 | (tcptbl, client) <- TCP.newClient crypto Left $ \case | ||
171 | Left v -> void . v . Just . (,) False | ||
172 | Right v -> \case | ||
173 | TCP.OnionPacketResponse x@(OnionAnnounceResponse n8 n24 _) -> do | ||
174 | mod <- lookupSender' pq rlog localhost4 n8 | ||
175 | perror $ "TCP announce response from " ++ show mod | ||
176 | forM_ mod $ \od -> do | ||
177 | Onion.decrypt crypto x od >>= \case | ||
178 | Right (y,_) -> do perror $ "decrypted announce response, sending " ++ show y | ||
179 | let | ||
180 | RouteId rid = fromMaybe (routeId (nodeId (onionNodeInfo od))) | ||
181 | $ onionRouteSpec od | ||
182 | Nonce8 w8 = n8 | ||
183 | atomically $ do | ||
184 | modifyTVar' pq (W64.delete w8) | ||
185 | modifyArray rm (fmap gotResponse) rid | ||
186 | void $ v $ Just y | ||
187 | _ -> return () | ||
188 | x -> perror $ "Unexpected TCP query result: " ++ show x | ||
189 | |||
190 | let addr = SockAddrInet 0 0 | ||
191 | tentative_udp = NodeInfo | ||
192 | { nodeId = key2id $ transportPublic crypto | ||
193 | , nodeIP = fromMaybe (toEnum 0) (fromSockAddr addr) | ||
194 | , nodePort = fromMaybe 0 $ sockAddrPort addr | ||
195 | } | ||
196 | tentative_info = TCP.NodeInfo tentative_udp (fromIntegral 443) | ||
197 | tbl <- atomically $ newTVar | ||
198 | $ R.nullTable (comparing TCP.nodeId) | ||
199 | (\s -> hashWithSalt s . TCP.nodeId) | ||
200 | tentative_info | ||
201 | R.defaultBucketCount | ||
202 | return $ (,) (tbl,tcptbl) TCP.TCPClient | ||
203 | { tcpCrypto = crypto | ||
204 | , tcpClient = client | ||
205 | , tcpGetGateway = \ni -> do | ||
206 | gw <- selectGateway tbl ni | ||
207 | writeTChan rlog $ unwords ["Selected TCP Gateway:",show ni,"via",show gw] | ||
208 | return gw | ||
209 | } | ||
210 | or <- atomically $ do | ||
211 | -- chan <- newTChan | ||
212 | drg <- newTVar drg0 | ||
213 | -- forM_ [0..11] $ \n -> writeTChan chan $ BuildRoute (RouteId n) | ||
214 | tn <- newTVar IntMap.empty | ||
215 | ti <- newTVar HashMap.empty | ||
216 | tc <- newTVar 0 | ||
217 | ttn <- newTVar IntMap.empty | ||
218 | tti <- newTVar HashMap.empty | ||
219 | ttc <- newTVar 0 | ||
220 | pr <- newArray (0,11) 0 | ||
221 | prober <- TCP.newProber | ||
222 | refresher <- newBucketRefresher | ||
223 | tbl | ||
224 | (TCP.nodeSearch prober tcp) | ||
225 | (fmap (maybe False $ const True) . TCP.tcpPing (TCP.tcpClient tcp)) | ||
226 | tcpmode <- newTVar $ if tcp_enabled then Just True else Nothing | ||
227 | let o = OnionRouter | ||
228 | { pendingRoutes = pr | ||
229 | , onionDRG = drg | ||
230 | , pendingQueries = pq | ||
231 | , routeMap = rm | ||
232 | , trampolinesUDP = TrampolineSet | ||
233 | { setNodes = tn | ||
234 | , setCount = tc | ||
235 | , setNodeClass = nodeClass | ||
236 | , setIDs = ti | ||
237 | } | ||
238 | , trampolinesTCP = TrampolineSet | ||
239 | { setNodes = ttn | ||
240 | , setCount = ttc | ||
241 | , setNodeClass = nodeClass . TCP.udpNodeInfo | ||
242 | , setIDs = tti | ||
243 | } | ||
244 | , tcpMode = tcpmode | ||
245 | , tcpKademliaClient = tcp | ||
246 | { TCP.tcpClient = | ||
247 | let c = TCP.tcpClient tcp | ||
248 | in c { clientNet = addHandler perror (handleMessage c) | ||
249 | $ onInbound (updateTCP o) | ||
250 | $ clientNet c } | ||
251 | } | ||
252 | , tcpBucketRefresher = refresher | ||
253 | , routeLog = rlog | ||
254 | , routeThread = error "forkRouteBuilder not invoked (missing onion route builder thread)." | ||
255 | , tcpKademliaThread = error "forkRouteBuilder not invoked (missing TCP bucket maintenance thread)." | ||
256 | , tcpProberState = tcpcons | ||
257 | , tcpProber = prober | ||
258 | , tcpProberThread = error "forkRouteBuilder not invoked (missing TCP probe thread)." | ||
259 | , routeLogger = perror | ||
260 | } | ||
261 | return o | ||
262 | return (or,tcptbl) | ||
263 | |||
264 | updateTCP :: OnionRouter -> TCP.NodeInfo -> p -> IO () | ||
265 | updateTCP or addr x = | ||
266 | let refresher = tcpBucketRefresher or | ||
267 | kademlia0 = refreshKademlia refresher | ||
268 | kademlia = kademlia0 { kademIO = (kademIO kademlia0) | ||
269 | { tblTransition = \tr -> do | ||
270 | case refresher of | ||
271 | BucketRefresher { refreshSearch = sch } -> do | ||
272 | let spc = searchSpace sch | ||
273 | bkts = refreshBuckets refresher | ||
274 | hookBucketList spc bkts or (trampolinesTCP or) tr | ||
275 | tblTransition (kademIO kademlia0) tr | ||
276 | } | ||
277 | } | ||
278 | in insertNode kademlia addr | ||
279 | |||
280 | selectGateway :: TVar (R.BucketList TCP.NodeInfo) -> NodeInfo -> STM (Maybe TCP.NodeInfo) | ||
281 | selectGateway tbl ni = do | ||
282 | ns <- kclosest TCP.tcpSpace 2 (nodeId ni) <$> readTVar tbl | ||
283 | return $ listToMaybe ns -- $ dropWhile (\n -> TCP.nodeId n == nodeId ni) ns | ||
284 | |||
285 | quitRouteBuilder :: OnionRouter -> IO () | ||
286 | quitRouteBuilder or = do | ||
287 | killThread (routeThread or) | ||
288 | killThread (tcpKademliaThread or) | ||
289 | killThread (tcpProberThread or) | ||
290 | |||
291 | forkRouteBuilder :: OnionRouter -> (NodeId -> NodeInfo -> IO (Maybe [NodeInfo])) -> IO OnionRouter | ||
292 | forkRouteBuilder or getnodes = do | ||
293 | bktsThread <- forkPollForRefresh $ tcpBucketRefresher or | ||
294 | tcpprobe <- forkIO $ TCP.runProbeQueue (tcpProber or) | ||
295 | (TCP.tcpClient $ tcpKademliaClient or) | ||
296 | 12 | ||
297 | labelThread tcpprobe "tcp-probe" | ||
298 | tid <- forkIO $ do | ||
299 | me <- myThreadId | ||
300 | labelThread me "OnionRouter" | ||
301 | forever $ do | ||
302 | let checkRebuild :: Int -> Int -> STM RouteEvent | ||
303 | checkRebuild rid wanted_ver = do | ||
304 | current_ver <- fmap routeVersion <$> readArray (routeMap or) rid | ||
305 | writeTChan (routeLog or) $ "ONION checkRebuild "++show(current_ver,wanted_ver) | ||
306 | check $ maybe True (< wanted_ver) current_ver | ||
307 | return $ BuildRoute $ RouteId rid | ||
308 | io <- atomically $ {-# SCC "forkRouteBuilder.log" #-} | ||
309 | (readTChan (routeLog or) >>= return . routeLogger or) | ||
310 | `orElse` {-# SCC "forkRouteBuilder.checkRebuild" #-} | ||
311 | (let stms = map (\rid -> checkRebuild rid =<< readArray (pendingRoutes or) rid) | ||
312 | [0..11] | ||
313 | in do event <- foldr1 orElse stms | ||
314 | return $ handleEvent getnodes or { routeThread = me } event) | ||
315 | io | ||
316 | return or { routeThread = tid | ||
317 | , tcpKademliaThread = bktsThread | ||
318 | , tcpProberThread = tcpprobe } | ||
319 | |||
320 | generateNodeId :: MonadRandom m => m NodeId | ||
321 | generateNodeId = either (error "unable to make random nodeid") | ||
322 | id | ||
323 | . S.decode <$> getRandomBytes 32 | ||
324 | |||
325 | distinct3by :: Eq t => (a -> t) -> a -> a -> a -> Bool | ||
326 | distinct3by f a b c = f a /= f b && f b /= f c && f c /= f a | ||
327 | |||
328 | -- The two integer functions below take an [inclusive,inclusive] range. | ||
329 | randomR :: (DRG g, Integral a) => (a, a) -> g -> (a, g) | ||
330 | randomR (l,h) = randomIvalInteger (toInteger l, toInteger h) | ||
331 | |||
332 | next :: DRG g => g -> (Int,g) | ||
333 | next g = withDRG g $ do bs <- getRandomBytes $ if fitsInInt (Proxy :: Proxy Word64) | ||
334 | then 8 | ||
335 | else 4 | ||
336 | either (return . error) return $ S.decode bs | ||
337 | |||
338 | randomIvalInteger :: (DRG g, Num a) => (Integer, Integer) -> g -> (a, g) | ||
339 | randomIvalInteger (l,h) rng | ||
340 | | l > h = randomIvalInteger (h,l) rng | ||
341 | | otherwise = case (f 1 0 rng) of (v, rng') -> (fromInteger (l + v `mod` k), rng') | ||
342 | where | ||
343 | (genlo, genhi) = (minBound :: Int, maxBound :: Int) -- genRange :: RandomGen g => g -> (Int, Int) | ||
344 | b = fromIntegral genhi - fromIntegral genlo + 1 | ||
345 | |||
346 | -- Probabilities of the most likely and least likely result | ||
347 | -- will differ at most by a factor of (1 +- 1/q). Assuming the RandomGen | ||
348 | -- is uniform, of course | ||
349 | |||
350 | -- On average, log q / log b more random values will be generated | ||
351 | -- than the minimum | ||
352 | q = 1000 | ||
353 | k = h - l + 1 | ||
354 | magtgt = k * q | ||
355 | |||
356 | -- generate random values until we exceed the target magnitude | ||
357 | f mag v g | mag >= magtgt = (v, g) | ||
358 | | otherwise = v' `seq`f (mag*b) v' g' where | ||
359 | (x,g') = next g -- next :: RandomGen g => g -> (Int, g) | ||
360 | v' = (v * b + (fromIntegral x - fromIntegral genlo)) | ||
361 | |||
362 | -- Repeatedly attempt to select 3 nodes as a secure onion route letting 1 second | ||
363 | -- elapse between retries. | ||
364 | -- | ||
365 | -- Only the DRG random seed is updated. Hopefully another thread will change the | ||
366 | -- trampolineNodes set so that selection can succeed. | ||
367 | selectTrampolines :: OnionRouter -> IO (Either [TCP.NodeInfo] [NodeInfo]) | ||
368 | selectTrampolines or = do | ||
369 | myThreadId >>= flip labelThread ("OnionRouter.selectTrampolines") | ||
370 | let tset :: (forall x. TrampolineSet x -> STM (Either [x] [x])) | ||
371 | -> STM (Either (Either [TCP.NodeInfo] [NodeInfo]) | ||
372 | (Either [TCP.NodeInfo] [NodeInfo])) | ||
373 | tset f = do | ||
374 | mm <- readTVar (tcpMode or) | ||
375 | -- TODO: better logic for deciding to use TCP or UDP trampolines. | ||
376 | if fromMaybe False mm | ||
377 | then left Left . right Left <$> f (trampolinesTCP or) | ||
378 | else left Right . right Right <$> f (trampolinesUDP or) | ||
379 | atomically (tset $ internalSelectTrampolines (onionDRG or)) >>= \case | ||
380 | Left ns -> do | ||
381 | -- atomically $ writeTChan (routeLog or) | ||
382 | routeLogger or $ unwords | ||
383 | ( "ONION Discarding insecure trampolines:" : (either (map show) (map show) ns)) | ||
384 | myThreadId >>= flip labelThread ("OnionRouter.selectTrampolines.sleep") | ||
385 | case ns of | ||
386 | Left [_,_,_] -> threadDelay 1000000 -- (tcp) wait 1 second if we failed the distinct3by predicate. | ||
387 | Right [_,_,_] -> threadDelay 1000000 -- (udp) wait 1 second if we failed the distinct3by predicate. | ||
388 | _ -> threadDelay 5000000 -- wait 5 seconds if insufficient nodes. | ||
389 | myThreadId >>= flip labelThread ("OnionRouter.selectTrampolines") | ||
390 | selectTrampolines or | ||
391 | Right ns -> do | ||
392 | myThreadId >>= flip labelThread ("OnionRouter") | ||
393 | return ns | ||
394 | |||
395 | data TrampolineSet ni = TrampolineSet | ||
396 | { -- | A set of nodes used to query for random route nodes. These aren't | ||
397 | -- used directly in onion routes, they are queried for route nodes that | ||
398 | -- are nearby randomly selected ids. | ||
399 | -- | ||
400 | -- These nodes are chosen from the kademlia buckets and when one of them | ||
401 | -- is evicted from a bucket, it is no longer used as a trampoline node. | ||
402 | setNodes :: TVar (IntMap ni) | ||
403 | -- | Indicates the current size of 'setNodes'. | ||
404 | , setCount :: TVar Int | ||
405 | -- | In order to reduce the likelihood that an attacker will control all | ||
406 | -- nodes in a route, we color the nodes with 'IPClass' and require | ||
407 | -- distinct colors for each of the hops. | ||
408 | , setNodeClass :: ni -> IPClass | ||
409 | -- | This map associates 'NodeId' values with the corresponding | ||
410 | -- 'trampolineNodes' index. | ||
411 | , setIDs :: TVar (HashMap NodeId Int) | ||
412 | } | ||
413 | |||
414 | choose3 :: (Integral a, DRG drg) => drg -> a -> ([a], drg) | ||
415 | choose3 drg0 cnt = ([a,b,c], drg) | ||
416 | where | ||
417 | (a, drg1) = randomR (0,cnt - 1) drg0 | ||
418 | (b0, drg2) = randomR (0,cnt - 2) drg1 | ||
419 | (c0, drg ) = randomR (0,cnt - 3) drg2 | ||
420 | b | b0 < a = b0 | ||
421 | | otherwise = b0 + 1 | ||
422 | [ac,bc] = sort [a,b] | ||
423 | c1 | c0 < ac = c0 | ||
424 | | otherwise = c0 + 1 | ||
425 | c | c1 < bc = c1 | ||
426 | | otherwise = c1 + 1 | ||
427 | |||
428 | -- Select 3 indices into the trampolineNodes set and returns the associated | ||
429 | -- nodes provided they are suitable for use in an onion route. Otherwise, it | ||
430 | -- returns Left with the nodes that were selected. | ||
431 | -- | ||
432 | -- The only write this function does to STM state is that the onionDRG random | ||
433 | -- seed will be updated. | ||
434 | internalSelectTrampolines :: TVar ChaChaDRG -> TrampolineSet ni -> STM (Either [ni] [ni]) | ||
435 | internalSelectTrampolines setDRG TrampolineSet{..} = do | ||
436 | cnt <- readTVar setCount | ||
437 | ts <- readTVar setNodes | ||
438 | drg0 <- readTVar setDRG | ||
439 | let ([a,b,c],drg) = choose3 drg0 cnt | ||
440 | ns = mapMaybe (\n -> IntMap.lookup n ts) [a,b,c] | ||
441 | ns' <- case ns of | ||
442 | [an,bn,cn] | distinct3by setNodeClass an bn cn | ||
443 | -> return $ Right ns | ||
444 | _ -> return $ Left ns | ||
445 | writeTVar setDRG drg | ||
446 | return ns' | ||
447 | |||
448 | handleEvent :: (NodeId -> NodeInfo -> IO (Maybe [NodeInfo])) -> OnionRouter -> RouteEvent -> IO () | ||
449 | handleEvent getnodes or e@(BuildRoute (RouteId rid)) = do | ||
450 | routeLogger or $ "ONION Rebuilding RouteId " ++ show rid | ||
451 | mb <- do | ||
452 | mts <- selectTrampolines or | ||
453 | join . atomically $ do | ||
454 | drg <- readTVar (onionDRG or) | ||
455 | av <- newTVar Nothing | ||
456 | bv <- newTVar Nothing | ||
457 | cv <- newTVar Nothing | ||
458 | let (getr, drg') = withDRG drg $ do | ||
459 | asec <- generateSecretKey -- Three aliases | ||
460 | bsec <- generateSecretKey | ||
461 | csec <- generateSecretKey | ||
462 | aq <- generateNodeId -- Three queries | ||
463 | bq <- generateNodeId | ||
464 | cq <- generateNodeId | ||
465 | sel <- B.head <$> getRandomBytes 1 -- Three two-bit result selectors (6 bits) | ||
466 | let asel = sel .&. 0x3 | ||
467 | bsel = shiftR sel 2 .&. 0x3 | ||
468 | csel = shiftR sel 4 .&. 0x3 | ||
469 | cycle' [] = [] | ||
470 | cycle' ns = cycle ns | ||
471 | sendq :: Word8 -> NodeId -> Int -> IO (Maybe NodeInfo) | ||
472 | sendq s q ni | ||
473 | | Right ts <- mts = (>>= (listToMaybe . drop (fromIntegral s) . cycle')) <$> getnodes q (ts !! ni) | ||
474 | | Left ts <- mts = case ni of | ||
475 | 0 -> return $ Just $ TCP.udpNodeInfo (ts !! 0) | ||
476 | n -> (>>= (listToMaybe . drop (fromIntegral s) . (\(ns,_,_)->cycle' ns))) | ||
477 | <$> TCP.getUDPNodes (tcpKademliaClient or) q (TCP.udpNodeInfo $ ts !! n) | ||
478 | sendqs = do | ||
479 | forkLabeled "sendq.asel" $ sendq asel aq 0 >>= atomically . writeTVar av . Just | ||
480 | forkLabeled "sendq.bsel" $ sendq bsel bq 1 >>= atomically . writeTVar bv . Just | ||
481 | forkLabeled "sendq.csel" $ sendq csel cq 2 >>= atomically . writeTVar cv . Just | ||
482 | -- This timeout should be unnecessary... But I'm paranoid. | ||
483 | -- Note: 10 seconds should be sufficient for typical get-nodes queries. | ||
484 | tm <- timeout 30000000 $ atomically $ do -- Wait for all 3 results. | ||
485 | rs <- catMaybes <$> sequence [readTVar av,readTVar bv,readTVar cv] | ||
486 | case rs of [_,_,_] -> do | ||
487 | return $ catMaybes $ rs | ||
488 | -- self <- IntMap.lookup (-1) <$> readTVar (trampolineNodes or) | ||
489 | -- return $ maybe (catMaybes rs) (\x -> [x,x,x]) self | ||
490 | _ -> retry | ||
491 | maybe (routeLogger or "ONION: Unexpected sendq timeout!" >> return []) | ||
492 | return | ||
493 | tm | ||
494 | return $ do | ||
495 | myThreadId >>= flip labelThread ("OnionRouter.sendqs") | ||
496 | let mtcpport = either (Just . TCP.tcpPort . head) (const Nothing) mts | ||
497 | nodes <- case mts of | ||
498 | Right [_,_,_] -> sendqs | ||
499 | Left [_,_,_] -> sendqs | ||
500 | _ -> return [] | ||
501 | myThreadId >>= flip labelThread ("OnionRouter") | ||
502 | routeLogger or $ unlines | ||
503 | [ "ONION trampolines: " ++ show mts | ||
504 | , "ONION query results: " ++ show nodes ] | ||
505 | case nodes of | ||
506 | [a,b,c] | distinct3by nodeClass a b c -> do | ||
507 | atomically $ do | ||
508 | writeTChan (routeLog or) $ unwords [ "ONION using route:" | ||
509 | , show $ nodeAddr a | ||
510 | , show $ nodeAddr b | ||
511 | , show $ nodeAddr c ] | ||
512 | return $ Just OnionRoute | ||
513 | { routeAliasA = asec | ||
514 | , routeAliasB = bsec | ||
515 | , routeAliasC = csec | ||
516 | , routeNodeA = a | ||
517 | , routeNodeB = b | ||
518 | , routeNodeC = c | ||
519 | , routeRelayPort = mtcpport | ||
520 | } | ||
521 | [a,b,c] -> do | ||
522 | atomically $ writeTChan (routeLog or) | ||
523 | $ unwords [ "ONION Discarding insecure route:" | ||
524 | , show $ nodeAddr a | ||
525 | , show $ nodeAddr b | ||
526 | , show $ nodeAddr c | ||
527 | ] | ||
528 | return Nothing | ||
529 | _ -> return Nothing | ||
530 | writeTVar (onionDRG or) drg' | ||
531 | return $ getr | ||
532 | now <- getPOSIXTime | ||
533 | atomically $ maybe (return ()) -- writeTVar (pendingRoutes or IntMap.! rid) True) | ||
534 | (\r -> do modifyArray (routeMap or) | ||
535 | (freshRoute now r) | ||
536 | rid | ||
537 | v <- routeVersion . fromJust <$> readArray (routeMap or) rid | ||
538 | writeArray (pendingRoutes or) rid v | ||
539 | ) | ||
540 | (mb :: Maybe OnionRoute) | ||
541 | case mb of | ||
542 | Just _ -> routeLogger or $ "ONION Finished RouteId " ++ show rid | ||
543 | Nothing -> routeLogger or $ "ONION Failed RouteId " ++ show rid | ||
544 | |||
545 | |||
546 | lookupSender :: OnionRouter -> SockAddr -> Nonce8 -> IO (Maybe (OnionDestination RouteId)) | ||
547 | lookupSender or = lookupSender' (pendingQueries or) (routeLog or) | ||
548 | |||
549 | lookupSender' :: TVar (Word64Map PendingQuery) | ||
550 | -> TChan String | ||
551 | -> SockAddr | ||
552 | -> Nonce8 | ||
553 | -> IO (Maybe (OnionDestination RouteId)) | ||
554 | lookupSender' pending log saddr (Nonce8 w8) = do | ||
555 | result <- atomically $ do | ||
556 | ks <- readTVar pending | ||
557 | let r = W64.lookup w8 ks | ||
558 | writeTChan log $ "ONION lookupSender " ++ unwords [show w8, "->", show r] | ||
559 | return r | ||
560 | return $ do | ||
561 | od <- result | ||
562 | let nid = nodeId $ onionNodeInfo $ pendingDestination od | ||
563 | ni <- either (const Nothing) Just $ nodeInfo nid saddr | ||
564 | Just (OnionDestination (onionAliasSelector $ pendingDestination od) | ||
565 | ni | ||
566 | (Just $ routeId nid)) | ||
567 | |||
568 | lookupRoute :: OnionRouter -> NodeInfo -> RouteId -> IO (Maybe OnionRoute) | ||
569 | lookupRoute or ni (RouteId rid) = do | ||
570 | mb <- atomically $ readArray (routeMap or) rid | ||
571 | return $ storedRoute <$> mb | ||
572 | |||
573 | lookupTimeout :: OnionRouter -> OnionDestination r -> STM (OnionDestination RouteId, Int) | ||
574 | lookupTimeout or (OnionDestination asel ni Nothing) = do | ||
575 | let RouteId rid = routeId (nodeId ni) | ||
576 | mrr <- readArray (routeMap or) rid | ||
577 | writeTChan (routeLog or) $ unwords ["ONION lookupTimeout " ,show rid] | ||
578 | case mrr of | ||
579 | Just rr -> return ( OnionDestination asel ni (Just $ RouteId rid), timeoutForRoute rr) | ||
580 | Nothing -> return ( OnionDestination asel ni Nothing , 0 ) | ||
581 | |||
582 | hookQueries :: OnionRouter -> (tid -> Nonce8) | ||
583 | -> TransactionMethods d tid (OnionDestination RouteId) x | ||
584 | -> TransactionMethods d tid (OnionDestination RouteId) x | ||
585 | hookQueries or t8 tmethods = TransactionMethods | ||
586 | { dispatchRegister = \nowPlusExpiry mvar od d -> {-# SCC "hookQ.dispatchRegister" #-} do -- :: MVar x -> d -> STM (tid, d) | ||
587 | let ni = onionNodeInfo od | ||
588 | rid@(RouteId ridn) = fromMaybe (routeId (nodeId ni)) $ onionRouteSpec od | ||
589 | wanted <- {-# SCC "hookQ.wanted" #-} (readArray (pendingRoutes or) ridn) | ||
590 | mr <- {-# SCC "hookQ.mr_action" #-} (readArray (routeMap or) ridn) | ||
591 | -- Block query until a route is ready. | ||
592 | check $ fromMaybe False $ do | ||
593 | RouteRecord{routeVersion=rv} <- {-# SCC "hookQ.mr" #-} mr | ||
594 | return $ wanted <= rv | ||
595 | (tid,d') <- dispatchRegister tmethods nowPlusExpiry mvar od d | ||
596 | let Nonce8 w8 = t8 tid | ||
597 | od' = case od of | ||
598 | OnionDestination {} -> od { onionRouteSpec = Just rid } | ||
599 | OnionToOwner a b -> OnionToOwner a b -- Type cast. | ||
600 | let pq = PendingQuery { pendingDestination = od' | ||
601 | , pendingVersion = maybe 0 routeVersion mr | ||
602 | } | ||
603 | pqs <- readTVar (pendingQueries or) | ||
604 | -- check $ W64.size pqs < 20 | ||
605 | modifyTVar' (pendingQueries or) (W64.insert w8 pq) | ||
606 | writeTChan (routeLog or) $ "ONION query add " ++ unwords [ show (Just $ pendingVersion pq,w8), ":=", show ni ] | ||
607 | return (tid,d') | ||
608 | , dispatchResponse = \tid x d -> {-# SCC "hookQ.dispatchResponse" #-} do -- :: tid -> x -> d -> STM (d, IO ()) | ||
609 | let Nonce8 w8 = t8 tid | ||
610 | mb <- W64.lookup w8 <$> readTVar (pendingQueries or) | ||
611 | modifyTVar' (pendingQueries or) (W64.delete w8) | ||
612 | forM_ mb $ \pq -> do | ||
613 | let od = pendingDestination pq | ||
614 | RouteId rid = fromMaybe (routeId (nodeId (onionNodeInfo od))) | ||
615 | $ onionRouteSpec od | ||
616 | modifyArray (routeMap or) (fmap gotResponse) rid | ||
617 | writeTChan (routeLog or) $ "ONION query del " ++ show (fmap pendingVersion mb, w8) | ||
618 | dispatchResponse tmethods tid x d | ||
619 | , dispatchCancel = \tid d -> {-# SCC "hookQ.dispatchCancel" #-} do -- :: tid -> d -> STM d | ||
620 | let Nonce8 w8 = t8 tid | ||
621 | mb <- W64.lookup w8 <$> readTVar (pendingQueries or) | ||
622 | modifyTVar' (pendingQueries or) (W64.delete w8) | ||
623 | forM_ mb $ \pq -> do | ||
624 | let od = pendingDestination pq | ||
625 | RouteId rid = fromMaybe (routeId (nodeId (onionNodeInfo od))) | ||
626 | $ onionRouteSpec od | ||
627 | mrr <- readArray (routeMap or) rid | ||
628 | forM_ mrr $ \rr -> do | ||
629 | when (routeVersion rr == pendingVersion pq) $ do | ||
630 | let expireRoute = modifyArray (pendingRoutes or) expire rid | ||
631 | expire ver | ver <= succ (pendingVersion pq) = succ (pendingVersion pq) | ||
632 | | otherwise = ver | ||
633 | modifyArray (routeMap or) (fmap gotTimeout) rid | ||
634 | case rr of | ||
635 | RouteRecord{ responseCount = 0 | ||
636 | , timeoutCount = c | ||
637 | , routeVersion = v } | c >= 5 -> expireRoute | ||
638 | RouteRecord{ responseCount = 1 | ||
639 | , timeoutCount = c | ||
640 | , routeVersion = v } | c >= 10 -> expireRoute | ||
641 | RouteRecord{ timeoutCount = c | ||
642 | , routeVersion = v } | c >= 20 -> expireRoute | ||
643 | _ -> return () | ||
644 | writeTChan (routeLog or) $ "ONION query can " ++ show (fmap pendingVersion mb, w8) | ||
645 | dispatchCancel tmethods tid d | ||
646 | } | ||
647 | |||
648 | |||
649 | -- hookBucketList :: KademliaSpace NodeId NodeInfo -> TVar (BucketList NodeInfo) -> OnionRouter -> RoutingTransition NodeInfo -> STM () | ||
650 | hookBucketList :: Show ni => | ||
651 | KademliaSpace NodeId ni | ||
652 | -> TVar (BucketList ni) | ||
653 | -> OnionRouter | ||
654 | -> TrampolineSet ni | ||
655 | -> RoutingTransition ni | ||
656 | -> STM () | ||
657 | hookBucketList kademlia bkts0 or TrampolineSet{..} (RoutingTransition ni Accepted) = do | ||
658 | (s,antibias) <- do | ||
659 | drg0 <- readTVar (onionDRG or) | ||
660 | bkts <- readTVar bkts0 | ||
661 | let antibias = 2 ^ bucketNumber kademlia (kademliaLocation kademlia ni) bkts | ||
662 | (s,drg) = randomR (0,antibias - 1) drg0 | ||
663 | writeTVar (onionDRG or) drg | ||
664 | {- | ||
665 | do -- Store localhost as trampoline node (-1). | ||
666 | -- This is potentionally useful for testing. | ||
667 | let self = (thisNode bkts) { nodeIP = read "127.0.0.1" } | ||
668 | modifyTVar' setNodes (IntMap.insert (-1) self) | ||
669 | -} | ||
670 | return (s::Int,antibias) | ||
671 | -- debias via stochastic filter | ||
672 | when (s == 0) $ do | ||
673 | ns <- readTVar setIDs -- (trampolineIds or) | ||
674 | case HashMap.lookup (kademliaLocation kademlia ni) ns of | ||
675 | Just _ -> return () | ||
676 | Nothing -> do | ||
677 | cnt <- readTVar setCount | ||
678 | writeTChan (routeLog or) $ "ONION trampoline Accepted " ++ unwords ["s="++show (s,antibias),show cnt, show ni] | ||
679 | modifyTVar' setIDs (HashMap.insert (kademliaLocation kademlia ni) cnt) | ||
680 | modifyTVar' setNodes (IntMap.insert cnt ni) | ||
681 | writeTVar setCount (succ cnt) | ||
682 | hookBucketList kademlia _ or TrampolineSet{..} (RoutingTransition ni Stranger) = do | ||
683 | ns <- readTVar setIDs | ||
684 | case HashMap.lookup (kademliaLocation kademlia ni) ns of | ||
685 | Just n -> do writeTVar setIDs (HashMap.delete (kademliaLocation kademlia ni) ns) | ||
686 | cnt <- pred <$> readTVar setCount | ||
687 | writeTVar setCount cnt | ||
688 | case compare n cnt of | ||
689 | EQ -> modifyTVar' setNodes (IntMap.delete n) | ||
690 | LT -> do lastnode <- (IntMap.! cnt) <$> readTVar setNodes | ||
691 | modifyTVar' setNodes | ||
692 | (IntMap.insert n lastnode . IntMap.delete cnt) | ||
693 | modifyTVar' setIDs | ||
694 | (HashMap.delete (kademliaLocation kademlia ni) | ||
695 | . HashMap.insert (kademliaLocation kademlia lastnode) n) | ||
696 | GT -> writeTChan (routeLog or) $ "BUG!! Trampoline maps are out of sync." | ||
697 | writeTChan (routeLog or) $ "ONION trampoline Stranger " ++ unwords [show n,show ni] | ||
698 | Nothing -> return () | ||
699 | hookBucketList _ _ _ _ _ = return () -- ignore Applicant event. | ||
700 | |||
701 | newtype IPClass = IPClass Word32 | ||
702 | deriving Eq | ||
703 | |||
704 | ipkey :: IPClass -> Int | ||
705 | ipkey (IPClass k) = fromIntegral k | ||
706 | |||
707 | nodeClass :: NodeInfo -> IPClass | ||
708 | nodeClass = ipClass . nodeAddr | ||
709 | |||
710 | ipClass :: SockAddr -> IPClass | ||
711 | ipClass= either ipClass' ipClass' . either4or6 | ||
712 | |||
713 | ipClass' :: SockAddr -> IPClass | ||
714 | ipClass' (SockAddrInet _ addr) = IPClass (fromBE32 addr .&. 0xFF000000) | ||
715 | ipClass' (SockAddrInet6 _ _ (hiword,_,_,_) _) = IPClass hiword | ||
716 | ipClass' _ = IPClass 0 -- unreachable. | ||
717 | |||
718 | requestTCPMode :: OnionRouter -> Maybe Bool -> IO Bool | ||
719 | requestTCPMode or wanted_mode = atomically $ requestTCPModeSTM or wanted_mode | ||
720 | |||
721 | requestTCPModeSTM :: OnionRouter -> Maybe Bool -> STM Bool | ||
722 | requestTCPModeSTM or wanted_mode = do | ||
723 | m <- readTVar (tcpMode or) | ||
724 | case m of | ||
725 | Nothing -> return False | ||
726 | Just oldmode -> case wanted_mode of | ||
727 | Just newmode -> do | ||
728 | writeTVar (tcpMode or) (Just newmode) | ||
729 | return newmode | ||
730 | Nothing -> return oldmode | ||